From ec235c3f14c90850983e1dcaf687033bc113d221 Mon Sep 17 00:00:00 2001 From: Nicholas Brennan Date: Thu, 11 Feb 2016 17:31:01 -0500 Subject: [PATCH] Audiveris Source Code Do not merge to this code. Copy it and build off it, but leave this all unchanged. --- .../installer/unix/UnixUtilitiesTest.java | 54 + .../installer/unix/VersionNumberTest.java | 70 + .../installer/AbstractCompanion.java | 352 ++ .../installer/BasicCompanionView.java | 243 ++ .../com/audiveris/installer/Bundle.java | 396 +++ .../com/audiveris/installer/BundleView.java | 425 +++ .../com/audiveris/installer/Companion.java | 120 + .../audiveris/installer/CompanionView.java | 49 + .../com/audiveris/installer/CppCompanion.java | 76 + .../com/audiveris/installer/Descriptor.java | 194 ++ .../installer/DescriptorFactory.java | 104 + .../com/audiveris/installer/DocCompanion.java | 278 ++ .../installer/ExamplesCompanion.java | 85 + .../com/audiveris/installer/Expander.java | 179 + .../com/audiveris/installer/FileCopier.java | 179 + .../audiveris/installer/FolderSelector.java | 206 ++ .../installer/GhostscriptCompanion.java | 75 + .../com/audiveris/installer/Installer.java | 313 ++ .../com/audiveris/installer/JarExpander.java | 226 ++ .../com/audiveris/installer/Jnlp.java | 598 ++++ .../installer/JnlpResponseCache.java | 93 + .../com/audiveris/installer/LangSelector.java | 400 +++ .../audiveris/installer/LicenseCompanion.java | 187 + .../com/audiveris/installer/LogUtilities.java | 161 + .../com/audiveris/installer/MessagePanel.java | 130 + .../com/audiveris/installer/OcrCompanion.java | 499 +++ .../audiveris/installer/PluginsCompanion.java | 91 + .../com/audiveris/installer/RegexUtil.java | 79 + .../com/audiveris/installer/SpecificFile.java | 53 + .../installer/TrainingCompanion.java | 91 + .../com/audiveris/installer/TreeRemover.java | 80 + .../UnsupportedEnvironmentException.java | 64 + .../com/audiveris/installer/Utilities.java | 322 ++ .../com/audiveris/installer/ViewAppender.java | 84 + .../com/audiveris/installer/mac/package.html | 14 + .../com/audiveris/installer/package.html | 13 + .../com/audiveris/installer/unix/Package.java | 186 + .../installer/unix/UnixDescriptor.java | 424 +++ .../installer/unix/UnixUtilities.java | 92 + .../installer/unix/VersionNumber.java | 380 +++ .../com/audiveris/installer/unix/package.html | 14 + .../installer/windows/WindowsDescriptor.java | 737 ++++ .../installer/windows/WindowsUtilities.java | 157 + .../audiveris/installer/windows/package.html | 14 + src/installer/doc-files/overview.uxf | 499 +++ src/installer/doc-files/roles.uxf | 585 ++++ .../InitializationErrorInvocationHandler.java | 58 + src/installer/hudson/util/jna/Kernel32.java | 93 + .../hudson/util/jna/Kernel32Utils.java | 136 + .../hudson/util/jna/SHELLEXECUTEINFO.java | 80 + src/installer/hudson/util/jna/Shell32.java | 44 + .../hudson/util/jna/WinIOException.java | 76 + src/installer/hudson/util/jna/package.html | 14 + src/installer/overview.html | 127 + src/main/Audiveris.java | 49 + src/main/omr/CLI.java | 676 ++++ src/main/omr/Debug.java | 173 + src/main/omr/Main.java | 548 +++ src/main/omr/WellKnowns.java | 501 +++ src/main/omr/action/ActionDescriptor.java | 115 + src/main/omr/action/ActionManager.java | 405 +++ src/main/omr/action/Actions.java | 211 ++ src/main/omr/action/package.html | 17 + .../omr/action/resources/Actions.properties | 36 + .../action/resources/Actions_fr.properties | 35 + src/main/omr/check/Check.java | 353 ++ src/main/omr/check/CheckBoard.java | 163 + src/main/omr/check/CheckPanel.java | 550 +++ src/main/omr/check/CheckResult.java | 30 + src/main/omr/check/CheckSuite.java | 351 ++ src/main/omr/check/Checkable.java | 33 + src/main/omr/check/FailureResult.java | 54 + src/main/omr/check/Result.java | 60 + src/main/omr/check/SuccessResult.java | 54 + src/main/omr/check/package.html | 21 + src/main/omr/constant/Constant.java | 923 +++++ src/main/omr/constant/ConstantManager.java | 453 +++ src/main/omr/constant/ConstantSet.java | 289 ++ src/main/omr/constant/Node.java | 103 + src/main/omr/constant/PackageNode.java | 100 + src/main/omr/constant/UnitManager.java | 525 +++ src/main/omr/constant/UnitModel.java | 498 +++ src/main/omr/constant/UnitNode.java | 96 + src/main/omr/constant/UnitTreeTable.java | 392 +++ src/main/omr/constant/doc-files/Constant.uxf | 30 + src/main/omr/constant/package.html | 82 + .../omr/glyph/AbstractEvaluationEngine.java | 459 +++ src/main/omr/glyph/BasicNest.java | 833 +++++ src/main/omr/glyph/CompoundBuilder.java | 462 +++ src/main/omr/glyph/Evaluation.java | 167 + src/main/omr/glyph/EvaluationEngine.java | 88 + src/main/omr/glyph/GlyphInspector.java | 285 ++ src/main/omr/glyph/GlyphNetwork.java | 551 +++ src/main/omr/glyph/GlyphRegression.java | 875 +++++ src/main/omr/glyph/GlyphRepository.java | 1071 ++++++ src/main/omr/glyph/GlyphSignature.java | 145 + src/main/omr/glyph/Glyphs.java | 663 ++++ src/main/omr/glyph/GlyphsBuilder.java | 583 ++++ src/main/omr/glyph/GlyphsModel.java | 370 ++ src/main/omr/glyph/Grades.java | 180 + src/main/omr/glyph/Nest.java | 235 ++ src/main/omr/glyph/SectionSets.java | 285 ++ src/main/omr/glyph/Shape.java | 723 ++++ src/main/omr/glyph/ShapeChecker.java | 1365 ++++++++ src/main/omr/glyph/ShapeDescription.java | 137 + src/main/omr/glyph/ShapeDescriptorART.java | 141 + src/main/omr/glyph/ShapeDescriptorGeo.java | 136 + src/main/omr/glyph/ShapeEvaluator.java | 146 + src/main/omr/glyph/ShapeSet.java | 848 +++++ src/main/omr/glyph/SymbolGlyph.java | 120 + src/main/omr/glyph/SymbolGlyphDescriptor.java | 272 ++ src/main/omr/glyph/SymbolsModel.java | 306 ++ src/main/omr/glyph/VirtualGlyph.java | 98 + .../omr/glyph/doc-files/GlyphEvaluator.uxf | 225 ++ src/main/omr/glyph/doc-files/Glyphs.uxf | 22 + .../omr/glyph/facets/BasicAdministration.java | 174 + src/main/omr/glyph/facets/BasicAlignment.java | 577 ++++ .../omr/glyph/facets/BasicComposition.java | 337 ++ src/main/omr/glyph/facets/BasicDisplay.java | 224 ++ .../omr/glyph/facets/BasicEnvironment.java | 368 ++ src/main/omr/glyph/facets/BasicFacet.java | 64 + src/main/omr/glyph/facets/BasicGeometry.java | 470 +++ src/main/omr/glyph/facets/BasicGlyph.java | 1103 ++++++ .../omr/glyph/facets/BasicRecognition.java | 310 ++ .../omr/glyph/facets/BasicTranslation.java | 119 + src/main/omr/glyph/facets/Glyph.java | 164 + .../omr/glyph/facets/GlyphAdministration.java | 95 + src/main/omr/glyph/facets/GlyphAlignment.java | 199 ++ .../omr/glyph/facets/GlyphComposition.java | 173 + src/main/omr/glyph/facets/GlyphContent.java | 113 + src/main/omr/glyph/facets/GlyphDisplay.java | 76 + .../omr/glyph/facets/GlyphEnvironment.java | 158 + src/main/omr/glyph/facets/GlyphFacet.java | 34 + src/main/omr/glyph/facets/GlyphGeometry.java | 202 ++ .../omr/glyph/facets/GlyphRecognition.java | 171 + .../omr/glyph/facets/GlyphTranslation.java | 61 + src/main/omr/glyph/facets/GlyphValue.java | 130 + src/main/omr/glyph/facets/package.html | 17 + src/main/omr/glyph/package.html | 108 + src/main/omr/glyph/pattern/AlterPattern.java | 671 ++++ .../glyph/pattern/ArticulationPattern.java | 141 + src/main/omr/glyph/pattern/BassPattern.java | 193 ++ .../omr/glyph/pattern/BeamHookPattern.java | 141 + .../omr/glyph/pattern/CaesuraPattern.java | 89 + src/main/omr/glyph/pattern/ClefPattern.java | 322 ++ src/main/omr/glyph/pattern/DotPattern.java | 254 ++ .../omr/glyph/pattern/DoubleBeamPattern.java | 137 + .../omr/glyph/pattern/FermataDotPattern.java | 141 + src/main/omr/glyph/pattern/FlagPattern.java | 144 + src/main/omr/glyph/pattern/FortePattern.java | 140 + src/main/omr/glyph/pattern/GlyphPattern.java | 76 + .../omr/glyph/pattern/HiddenSlurPattern.java | 104 + src/main/omr/glyph/pattern/LedgerPattern.java | 273 ++ .../omr/glyph/pattern/LeftOverPattern.java | 114 + .../omr/glyph/pattern/PatternsChecker.java | 179 + src/main/omr/glyph/pattern/SlurInspector.java | 1427 ++++++++ src/main/omr/glyph/pattern/SplitPattern.java | 400 +++ src/main/omr/glyph/pattern/StemPattern.java | 202 ++ src/main/omr/glyph/pattern/TimePattern.java | 248 ++ src/main/omr/glyph/pattern/package.html | 18 + src/main/omr/glyph/ui/AttachmentHolder.java | 61 + .../omr/glyph/ui/BasicAttachmentHolder.java | 118 + src/main/omr/glyph/ui/EvaluationBoard.java | 488 +++ src/main/omr/glyph/ui/GlyphBoard.java | 596 ++++ src/main/omr/glyph/ui/GlyphBrowser.java | 927 +++++ src/main/omr/glyph/ui/GlyphMenu.java | 704 ++++ src/main/omr/glyph/ui/GlyphsController.java | 306 ++ src/main/omr/glyph/ui/NestView.java | 467 +++ src/main/omr/glyph/ui/SampleVerifier.java | 740 ++++ src/main/omr/glyph/ui/ShapeBoard.java | 587 ++++ src/main/omr/glyph/ui/ShapeColorChooser.java | 555 +++ src/main/omr/glyph/ui/ShapeFocusBoard.java | 515 +++ src/main/omr/glyph/ui/SpinnerGlyphModel.java | 229 ++ src/main/omr/glyph/ui/SymbolGlyphBoard.java | 466 +++ src/main/omr/glyph/ui/SymbolMenu.java | 507 +++ src/main/omr/glyph/ui/SymbolsBlackList.java | 47 + src/main/omr/glyph/ui/SymbolsController.java | 220 ++ src/main/omr/glyph/ui/SymbolsEditor.java | 656 ++++ .../omr/glyph/ui/UserEventSubscriber.java | 26 + src/main/omr/glyph/ui/ViewParameters.java | 303 ++ .../glyph/ui/doc-files/GlyphsController.uxf | 57 + .../glyph/ui/doc-files/SymbolGlyphBoard.png | Bin 0 -> 59884 bytes src/main/omr/glyph/ui/package.html | 129 + src/main/omr/glyph/ui/panel/GlyphTrainer.java | 310 ++ src/main/omr/glyph/ui/panel/NetworkPanel.java | 600 ++++ .../omr/glyph/ui/panel/RegressionPanel.java | 84 + .../omr/glyph/ui/panel/SelectionPanel.java | 603 ++++ .../omr/glyph/ui/panel/TrainingPanel.java | 508 +++ .../omr/glyph/ui/panel/ValidationPanel.java | 389 +++ src/main/omr/glyph/ui/panel/package.html | 16 + .../panel/resources/GlyphTrainer.properties | 11 + .../resources/GlyphTrainer_fr_FR.properties | 11 + .../ui/resources/SampleVerifier.properties | 11 + .../ui/resources/SampleVerifier_fr.properties | 11 + .../ui/resources/ShapeColorChooser.properties | 11 + .../resources/ShapeColorChooser_fr.properties | 11 + .../ui/resources/ViewParameters.properties | 28 + .../ui/resources/ViewParameters_fr.properties | 27 + src/main/omr/graph/BasicDigraph.java | 243 ++ src/main/omr/graph/BasicVertex.java | 376 +++ src/main/omr/graph/Digraph.java | 121 + src/main/omr/graph/DigraphView.java | 36 + src/main/omr/graph/Vertex.java | 146 + src/main/omr/graph/VertexView.java | 43 + src/main/omr/graph/package.html | 17 + src/main/omr/grid/BarAlignment.java | 199 ++ src/main/omr/grid/BarInfo.java | 101 + src/main/omr/grid/BarsRetriever.java | 1868 ++++++++++ src/main/omr/grid/ClustersRetriever.java | 1197 +++++++ src/main/omr/grid/Filament.java | 224 ++ src/main/omr/grid/FilamentAlignment.java | 591 ++++ src/main/omr/grid/FilamentComb.java | 180 + src/main/omr/grid/FilamentLine.java | 292 ++ src/main/omr/grid/FilamentsFactory.java | 1007 ++++++ src/main/omr/grid/GridBuilder.java | 266 ++ src/main/omr/grid/IntersectionSequence.java | 95 + src/main/omr/grid/LagWeaver.java | 685 ++++ src/main/omr/grid/LineCluster.java | 756 +++++ src/main/omr/grid/LineFilament.java | 214 ++ src/main/omr/grid/LineFilamentAlignment.java | 293 ++ src/main/omr/grid/LineInfo.java | 109 + src/main/omr/grid/LinesRetriever.java | 1028 ++++++ src/main/omr/grid/RunsViewer.java | 117 + src/main/omr/grid/StaffInfo.java | 986 ++++++ src/main/omr/grid/StaffManager.java | 352 ++ src/main/omr/grid/StickIntersection.java | 149 + src/main/omr/grid/TargetBuilder.java | 486 +++ src/main/omr/grid/TargetLine.java | 138 + src/main/omr/grid/TargetPage.java | 71 + src/main/omr/grid/TargetStaff.java | 79 + src/main/omr/grid/TargetSystem.java | 92 + src/main/omr/grid/doc-files/cluster.uxf | 237 ++ src/main/omr/grid/doc-files/filament.uxf | 367 ++ src/main/omr/grid/doc-files/grid.uxf | 189 ++ src/main/omr/grid/doc-files/pixel.uxf | 387 +++ src/main/omr/grid/doc-files/target.uxf | 122 + src/main/omr/grid/package.html | 31 + src/main/omr/lag/BasicLag.java | 616 ++++ src/main/omr/lag/BasicRoi.java | 209 ++ src/main/omr/lag/BasicSection.java | 1719 ++++++++++ src/main/omr/lag/JunctionAllPolicy.java | 65 + src/main/omr/lag/JunctionDeltaPolicy.java | 80 + src/main/omr/lag/JunctionPolicy.java | 41 + src/main/omr/lag/JunctionRatioPolicy.java | 87 + src/main/omr/lag/Lag.java | 188 ++ src/main/omr/lag/Roi.java | 77 + src/main/omr/lag/Section.java | 601 ++++ src/main/omr/lag/SectionSignature.java | 106 + src/main/omr/lag/Sections.java | 221 ++ src/main/omr/lag/SectionsBuilder.java | 347 ++ src/main/omr/lag/package.html | 19 + src/main/omr/lag/ui/SectionBoard.java | 444 +++ src/main/omr/lag/ui/SectionView.java | 74 + src/main/omr/lag/ui/SpinnerSectionModel.java | 156 + src/main/omr/lag/ui/package.html | 18 + src/main/omr/log/LogGuiAppender.java | 77 + src/main/omr/log/LogPane.java | 194 ++ src/main/omr/log/LogStepAppender.java | 41 + src/main/omr/log/LogUtil.java | 188 ++ src/main/omr/log/LoggingStream.java | 79 + src/main/omr/log/package.html | 16 + src/main/omr/math/Barycenter.java | 180 + src/main/omr/math/BasicLine.java | 518 +++ src/main/omr/math/Circle.java | 579 ++++ src/main/omr/math/Ellipse.java | 609 ++++ src/main/omr/math/GCD.java | 147 + src/main/omr/math/GeoPath.java | 408 +++ src/main/omr/math/GeoUtil.java | 55 + src/main/omr/math/Histogram.java | 757 +++++ src/main/omr/math/InjectionSolver.java | 161 + src/main/omr/math/IntegerHistogram.java | 115 + src/main/omr/math/Line.java | 182 + src/main/omr/math/LineUtil.java | 98 + src/main/omr/math/LinearEvaluator.java | 1122 ++++++ src/main/omr/math/NaturalSpline.java | 427 +++ src/main/omr/math/NeuralNetwork.java | 929 +++++ src/main/omr/math/PointsCollector.java | 182 + src/main/omr/math/Polynomial.java | 346 ++ src/main/omr/math/Population.java | 191 ++ src/main/omr/math/Rational.java | 378 +++ src/main/omr/math/ReversePathIterator.java | 492 +++ src/main/omr/math/package.html | 17 + src/main/omr/moments/ARTMoments.java | 117 + src/main/omr/moments/AbstractExtractor.java | 141 + src/main/omr/moments/BasicARTExtractor.java | 163 + src/main/omr/moments/BasicARTMoments.java | 197 ++ src/main/omr/moments/BasicLUT.java | 147 + .../omr/moments/BasicLegendreExtractor.java | 331 ++ .../omr/moments/BasicLegendreMoments.java | 111 + src/main/omr/moments/GeometricMoments.java | 400 +++ src/main/omr/moments/LUT.java | 77 + src/main/omr/moments/LegendreMoments.java | 28 + src/main/omr/moments/MomentsExtractor.java | 54 + src/main/omr/moments/OrthogonalMoments.java | 63 + src/main/omr/moments/QuantizedARTMoments.java | 212 ++ src/main/omr/moments/doc-files/moments.uxf | 301 ++ src/main/omr/moments/package.html | 18 + src/main/omr/package.html | 16 + .../JavascriptUnavailableException.java | 23 + src/main/omr/plugin/Plugin.java | 317 ++ src/main/omr/plugin/PluginAction.java | 73 + src/main/omr/plugin/PluginsManager.java | 366 ++ src/main/omr/plugin/package.html | 14 + .../resources/PluginsManager.properties | 13 + .../resources/PluginsManager_fr.properties | 12 + src/main/omr/run/AdaptiveDescriptor.java | 168 + src/main/omr/run/AdaptiveFilter.java | 436 +++ src/main/omr/run/FilterDescriptor.java | 204 ++ src/main/omr/run/FilterKind.java | 104 + src/main/omr/run/GlobalDescriptor.java | 125 + src/main/omr/run/GlobalFilter.java | 120 + src/main/omr/run/Orientation.java | 234 ++ src/main/omr/run/Oriented.java | 30 + src/main/omr/run/PixelFilter.java | 68 + src/main/omr/run/PixelSource.java | 57 + src/main/omr/run/PixelsBuffer.java | 121 + src/main/omr/run/RandomFilter.java | 93 + src/main/omr/run/Run.java | 226 ++ src/main/omr/run/RunBoard.java | 199 ++ src/main/omr/run/RunsRetriever.java | 302 ++ src/main/omr/run/RunsTable.java | 723 ++++ src/main/omr/run/RunsTableFactory.java | 216 ++ src/main/omr/run/RunsTableView.java | 225 ++ src/main/omr/run/SourceWrapper.java | 67 + src/main/omr/run/VerticalFilter.java | 142 + src/main/omr/run/package.html | 17 + src/main/omr/score/DurationRetriever.java | 194 ++ src/main/omr/score/KeySignatureVerifier.java | 399 +++ src/main/omr/score/MeasureBasicNumberer.java | 100 + src/main/omr/score/MeasureFixer.java | 456 +++ src/main/omr/score/MusicXML.java | 373 ++ src/main/omr/score/PageReduction.java | 98 + src/main/omr/score/PartConnection.java | 973 ++++++ src/main/omr/score/Score.java | 998 ++++++ src/main/omr/score/ScoreBench.java | 270 ++ src/main/omr/score/ScoreChecker.java | 959 ++++++ src/main/omr/score/ScoreCleaner.java | 124 + src/main/omr/score/ScoreExporter.java | 2996 +++++++++++++++++ src/main/omr/score/ScoreReduction.java | 220 ++ src/main/omr/score/ScoreReductor.java | 121 + src/main/omr/score/ScoreXmlReduction.java | 985 ++++++ src/main/omr/score/ScoresManager.java | 656 ++++ src/main/omr/score/SlotBuilder.java | 843 +++++ src/main/omr/score/SystemTranslator.java | 1151 +++++++ src/main/omr/score/TimeSignatureFixer.java | 337 ++ .../omr/score/TimeSignatureRetriever.java | 351 ++ src/main/omr/score/doc-files/Part.uxf | 40 + .../omr/score/entity/AbstractDirection.java | 83 + .../omr/score/entity/AbstractNotation.java | 62 + src/main/omr/score/entity/Arpeggiate.java | 150 + src/main/omr/score/entity/Articulation.java | 165 + src/main/omr/score/entity/Barline.java | 464 +++ src/main/omr/score/entity/Beam.java | 667 ++++ src/main/omr/score/entity/BeamGroup.java | 755 +++++ src/main/omr/score/entity/BeamItem.java | 467 +++ src/main/omr/score/entity/Chord.java | 1919 +++++++++++ src/main/omr/score/entity/ChordInfo.java | 990 ++++++ src/main/omr/score/entity/ChordSymbol.java | 123 + src/main/omr/score/entity/Clef.java | 380 +++ src/main/omr/score/entity/Coda.java | 88 + src/main/omr/score/entity/Container.java | 55 + src/main/omr/score/entity/Direction.java | 52 + .../omr/score/entity/DirectionStatement.java | 99 + src/main/omr/score/entity/DotTranslation.java | 636 ++++ src/main/omr/score/entity/DurationFactor.java | 65 + src/main/omr/score/entity/Dynamics.java | 286 ++ src/main/omr/score/entity/Fermata.java | 114 + src/main/omr/score/entity/KeySignature.java | 1041 ++++++ src/main/omr/score/entity/LyricsItem.java | 436 +++ src/main/omr/score/entity/LyricsLine.java | 310 ++ src/main/omr/score/entity/Mark.java | 151 + src/main/omr/score/entity/Measure.java | 1946 +++++++++++ src/main/omr/score/entity/MeasureElement.java | 267 ++ src/main/omr/score/entity/MeasureId.java | 577 ++++ src/main/omr/score/entity/MeasureNode.java | 118 + src/main/omr/score/entity/Notation.java | 42 + src/main/omr/score/entity/Note.java | 1239 +++++++ src/main/omr/score/entity/Ornament.java | 112 + src/main/omr/score/entity/Page.java | 481 +++ src/main/omr/score/entity/PageNode.java | 93 + src/main/omr/score/entity/PartNode.java | 249 ++ src/main/omr/score/entity/Pedal.java | 93 + src/main/omr/score/entity/ScoreNode.java | 83 + src/main/omr/score/entity/ScorePart.java | 317 ++ src/main/omr/score/entity/ScoreSystem.java | 603 ++++ src/main/omr/score/entity/Segno.java | 91 + src/main/omr/score/entity/Slot.java | 648 ++++ src/main/omr/score/entity/Slur.java | 965 ++++++ src/main/omr/score/entity/Staff.java | 393 +++ src/main/omr/score/entity/SystemNode.java | 363 ++ src/main/omr/score/entity/SystemPart.java | 1086 ++++++ src/main/omr/score/entity/Tempo.java | 84 + src/main/omr/score/entity/Text.java | 533 +++ src/main/omr/score/entity/TimeRational.java | 114 + src/main/omr/score/entity/TimeSignature.java | 951 ++++++ src/main/omr/score/entity/Tuplet.java | 567 ++++ src/main/omr/score/entity/VisitableNode.java | 93 + src/main/omr/score/entity/Voice.java | 687 ++++ src/main/omr/score/entity/Wedge.java | 140 + src/main/omr/score/entity/doc-files/Beam.uxf | 22 + .../score/entity/doc-files/Note-Pack-both.png | Bin 0 -> 551 bytes .../omr/score/entity/doc-files/Note-Pack.png | Bin 0 -> 791 bytes src/main/omr/score/entity/doc-files/Slot.png | Bin 0 -> 4407 bytes .../entity/doc-files/TextTranslation.uxf | 36 + .../entity/doc-files/Visitable-Hierarchy.uxf | 1589 +++++++++ src/main/omr/score/entity/package.html | 18 + src/main/omr/score/midi/MidiAbstractions.java | 339 ++ src/main/omr/score/midi/doc-files/midi.uxf | 80 + src/main/omr/score/midi/package.html | 21 + src/main/omr/score/package.html | 17 + src/main/omr/score/ui/BarPainter.java | 344 ++ src/main/omr/score/ui/PageMenu.java | 581 ++++ src/main/omr/score/ui/PagePainter.java | 1282 +++++++ .../omr/score/ui/PagePhysicalPainter.java | 716 ++++ src/main/omr/score/ui/PaintingLayer.java | 43 + src/main/omr/score/ui/PaintingParameters.java | 389 +++ src/main/omr/score/ui/ScoreActions.java | 675 ++++ src/main/omr/score/ui/ScoreController.java | 67 + src/main/omr/score/ui/ScoreDependent.java | 157 + src/main/omr/score/ui/ScoreParameters.java | 1437 ++++++++ src/main/omr/score/ui/ScoreTree.java | 633 ++++ src/main/omr/score/ui/SheetPdfOutput.java | 121 + .../score/ui/doc-files/ScoreParameters.png | Bin 0 -> 30167 bytes src/main/omr/score/ui/doc-files/ScoreView.uxf | 63 + src/main/omr/score/ui/package.html | 19 + .../resources/PaintingParameters.properties | 33 + .../PaintingParameters_fr.properties | 28 + .../ui/resources/ScoreActions.properties | 55 + .../ui/resources/ScoreActions_fr.properties | 45 + .../score/ui/resources/ScoreTree.properties | 14 + .../ui/resources/ScoreTree_fr.properties | 11 + .../score/visitor/AbstractScoreVisitor.java | 358 ++ src/main/omr/score/visitor/ScoreVisitor.java | 132 + src/main/omr/score/visitor/Visitable.java | 33 + src/main/omr/score/visitor/package.html | 17 + src/main/omr/script/AssignTask.java | 178 + src/main/omr/script/BarlineTask.java | 147 + src/main/omr/script/BoundaryTask.java | 145 + src/main/omr/script/DeleteTask.java | 216 ++ src/main/omr/script/ExportTask.java | 87 + src/main/omr/script/GlyphTask.java | 251 ++ src/main/omr/script/GlyphUpdateTask.java | 110 + src/main/omr/script/InsertTask.java | 275 ++ src/main/omr/script/ParametersTask.java | 456 +++ src/main/omr/script/PrintTask.java | 82 + src/main/omr/script/RationalTask.java | 100 + src/main/omr/script/RemoveTask.java | 69 + src/main/omr/script/Script.java | 315 ++ src/main/omr/script/ScriptActions.java | 385 +++ src/main/omr/script/ScriptManager.java | 196 ++ src/main/omr/script/ScriptTask.java | 196 ++ src/main/omr/script/SegmentTask.java | 88 + src/main/omr/script/SheetTask.java | 87 + src/main/omr/script/SlurTask.java | 79 + src/main/omr/script/StepTask.java | 118 + src/main/omr/script/TextTask.java | 118 + src/main/omr/script/doc-files/script.uxf | 30 + src/main/omr/script/package.html | 17 + .../script/resources/ScriptActions.properties | 21 + .../resources/ScriptActions_fr.properties | 18 + src/main/omr/selection/GlyphEvent.java | 59 + src/main/omr/selection/GlyphIdEvent.java | 54 + src/main/omr/selection/GlyphSetEvent.java | 75 + src/main/omr/selection/LagEvent.java | 38 + src/main/omr/selection/LocationEvent.java | 69 + src/main/omr/selection/MouseMovement.java | 40 + src/main/omr/selection/NestEvent.java | 38 + src/main/omr/selection/PixelLevelEvent.java | 57 + src/main/omr/selection/RunEvent.java | 59 + src/main/omr/selection/SectionEvent.java | 59 + src/main/omr/selection/SectionIdEvent.java | 52 + src/main/omr/selection/SectionSetEvent.java | 75 + src/main/omr/selection/SelectionHint.java | 127 + src/main/omr/selection/SelectionService.java | 251 ++ src/main/omr/selection/SheetEvent.java | 60 + src/main/omr/selection/UserEvent.java | 145 + src/main/omr/selection/doc-files/Events.uxf | 559 +++ src/main/omr/selection/doc-files/Flows.uxf | 916 +++++ src/main/omr/selection/package.html | 23 + src/main/omr/sheet/BarsChecker.java | 1351 ++++++++ src/main/omr/sheet/Bench.java | 93 + src/main/omr/sheet/BorderBuilder.java | 704 ++++ src/main/omr/sheet/BrokenLineContext.java | 91 + src/main/omr/sheet/Dash.java | 212 ++ src/main/omr/sheet/Ending.java | 43 + src/main/omr/sheet/Horizontals.java | 110 + src/main/omr/sheet/HorizontalsBuilder.java | 1493 ++++++++ src/main/omr/sheet/Ledger.java | 102 + src/main/omr/sheet/MeasuresBuilder.java | 443 +++ src/main/omr/sheet/NotePosition.java | 114 + src/main/omr/sheet/PartInfo.java | 57 + src/main/omr/sheet/Scale.java | 590 ++++ src/main/omr/sheet/ScaleBuilder.java | 832 +++++ src/main/omr/sheet/Sheet.java | 1532 +++++++++ src/main/omr/sheet/SheetBench.java | 227 ++ src/main/omr/sheet/Skew.java | 151 + src/main/omr/sheet/SystemBoundary.java | 236 ++ src/main/omr/sheet/SystemInfo.java | 1582 +++++++++ src/main/omr/sheet/SystemsBuilder.java | 378 +++ src/main/omr/sheet/VerticalsBuilder.java | 615 ++++ src/main/omr/sheet/VerticalsController.java | 243 ++ src/main/omr/sheet/package.html | 146 + src/main/omr/sheet/picture/Ghostscript.java | 226 ++ .../sheet/picture/ImageFormatException.java | 33 + src/main/omr/sheet/picture/Picture.java | 573 ++++ src/main/omr/sheet/picture/PictureLoader.java | 292 ++ src/main/omr/sheet/picture/PictureView.java | 149 + .../omr/sheet/picture/jai/JaiDewarper.java | 92 + src/main/omr/sheet/picture/jai/JaiLoader.java | 117 + src/main/omr/sheet/picture/package.html | 16 + .../resources/LedgerParameters.properties | 12 + .../resources/LedgerParameters_fr.properties | 12 + .../resources/LinesParameters.properties | 12 + .../resources/LinesParameters_fr.properties | 12 + src/main/omr/sheet/ui/BinarizationBoard.java | 193 ++ src/main/omr/sheet/ui/BoundaryEditor.java | 348 ++ src/main/omr/sheet/ui/PixelBoard.java | 241 ++ src/main/omr/sheet/ui/ScoreColorizer.java | 117 + src/main/omr/sheet/ui/SheetActions.java | 427 +++ src/main/omr/sheet/ui/SheetAssembly.java | 634 ++++ src/main/omr/sheet/ui/SheetDependent.java | 116 + src/main/omr/sheet/ui/SheetPainter.java | 286 ++ src/main/omr/sheet/ui/SheetsController.java | 453 +++ src/main/omr/sheet/ui/package.html | 16 + .../ui/resources/SheetActions.properties | 43 + .../ui/resources/SheetActions_fr.properties | 33 + src/main/omr/step/AbstractStep.java | 247 ++ src/main/omr/step/AbstractSystemStep.java | 216 ++ src/main/omr/step/ExportStep.java | 67 + src/main/omr/step/GridStep.java | 58 + src/main/omr/step/LoadStep.java | 82 + src/main/omr/step/MeasuresStep.java | 84 + src/main/omr/step/PagesStep.java | 180 + src/main/omr/step/PluginStep.java | 120 + src/main/omr/step/PrintStep.java | 67 + .../step/ProcessingCancellationException.java | 56 + src/main/omr/step/ScaleStep.java | 67 + src/main/omr/step/ScoreStep.java | 128 + src/main/omr/step/SheetTask.java | 152 + src/main/omr/step/Step.java | 144 + src/main/omr/step/StepException.java | 53 + src/main/omr/step/StepMenu.java | 263 ++ src/main/omr/step/StepMonitor.java | 231 ++ src/main/omr/step/Stepping.java | 656 ++++ src/main/omr/step/Steps.java | 332 ++ src/main/omr/step/SticksStep.java | 94 + src/main/omr/step/SymbolsStep.java | 144 + src/main/omr/step/SystemsStep.java | 77 + src/main/omr/step/TextsStep.java | 65 + src/main/omr/step/doc-files/Step.uxf | 681 ++++ src/main/omr/step/package.html | 21 + src/main/omr/stick/SectionRole.java | 104 + src/main/omr/stick/SectionsSource.java | 155 + src/main/omr/stick/StickRelation.java | 135 + src/main/omr/stick/SticksBuilder.java | 1002 ++++++ .../omr/stick/UnknownSectionPredicate.java | 48 + src/main/omr/stick/package.html | 20 + src/main/omr/text/BasicContent.java | 294 ++ src/main/omr/text/FontInfo.java | 149 + src/main/omr/text/Language.java | 349 ++ src/main/omr/text/OCR.java | 88 + src/main/omr/text/TextBasedItem.java | 233 ++ src/main/omr/text/TextBuilder.java | 1275 +++++++ src/main/omr/text/TextChar.java | 42 + src/main/omr/text/TextCheckerPattern.java | 177 + src/main/omr/text/TextItem.java | 211 ++ src/main/omr/text/TextLine.java | 639 ++++ src/main/omr/text/TextPattern.java | 277 ++ src/main/omr/text/TextRole.java | 334 ++ src/main/omr/text/TextRoleInfo.java | 94 + src/main/omr/text/TextScanner.java | 367 ++ src/main/omr/text/TextWord.java | 493 +++ src/main/omr/text/WordScanner.java | 294 ++ .../omr/text/doc-files/Text Processing.uxf | 263 ++ src/main/omr/text/doc-files/Text.uxf | 265 ++ src/main/omr/text/package.html | 59 + src/main/omr/text/tesseract/TesseractOCR.java | 239 ++ .../omr/text/tesseract/TesseractOrder.java | 372 ++ src/main/omr/text/tesseract/package.html | 14 + src/main/omr/ui/Board.java | 467 +++ src/main/omr/ui/BoardsPane.java | 402 +++ src/main/omr/ui/Colors.java | 133 + src/main/omr/ui/EntityAction.java | 149 + src/main/omr/ui/ErrorsEditor.java | 414 +++ src/main/omr/ui/FileDropHandler.java | 252 ++ src/main/omr/ui/GuiActions.java | 716 ++++ src/main/omr/ui/MacApplication.java | 247 ++ src/main/omr/ui/MainGui.java | 820 +++++ src/main/omr/ui/MemoryMeter.java | 259 ++ src/main/omr/ui/OmrUIDefaults.java | 139 + src/main/omr/ui/Options.java | 275 ++ src/main/omr/ui/PixelCount.java | 38 + .../omr/ui/dnd/AbstractGhostDropListener.java | 103 + .../omr/ui/dnd/GhostComponentAdapter.java | 69 + src/main/omr/ui/dnd/GhostDropAdapter.java | 146 + src/main/omr/ui/dnd/GhostDropEvent.java | 74 + src/main/omr/ui/dnd/GhostDropListener.java | 31 + src/main/omr/ui/dnd/GhostGlassPane.java | 194 ++ src/main/omr/ui/dnd/GhostImageAdapter.java | 47 + src/main/omr/ui/dnd/GhostMotionAdapter.java | 70 + src/main/omr/ui/dnd/GhostPictureAdapter.java | 59 + src/main/omr/ui/dnd/ScreenPoint.java | 111 + src/main/omr/ui/dnd/doc-files/DnD.uxf | 64 + src/main/omr/ui/dnd/package.html | 23 + src/main/omr/ui/field/IntegerListSpinner.java | 45 + src/main/omr/ui/field/LComboBox.java | 83 + src/main/omr/ui/field/LDoubleField.java | 151 + src/main/omr/ui/field/LField.java | 115 + src/main/omr/ui/field/LHexaSpinner.java | 133 + src/main/omr/ui/field/LIntegerField.java | 93 + src/main/omr/ui/field/LIntegerSpinner.java | 98 + src/main/omr/ui/field/LSpinner.java | 116 + src/main/omr/ui/field/LTextField.java | 98 + src/main/omr/ui/field/SpinnerUtil.java | 134 + src/main/omr/ui/field/doc-files/Fields.uxf | 49 + src/main/omr/ui/field/package.html | 16 + src/main/omr/ui/package.html | 17 + .../omr/ui/resources/GuiActions.properties | 105 + .../omr/ui/resources/GuiActions_fr.properties | 75 + src/main/omr/ui/resources/MainGui.properties | 47 + .../omr/ui/resources/MainGui_fr.properties | 15 + .../omr/ui/resources/MemoryMeter.properties | 18 + src/main/omr/ui/resources/Options.properties | 23 + .../omr/ui/resources/Options_fr_FR.properties | 20 + src/main/omr/ui/resources/icon-16.png | Bin 0 -> 536 bytes src/main/omr/ui/resources/icon-24.png | Bin 0 -> 936 bytes src/main/omr/ui/resources/icon-256.png | Bin 0 -> 12082 bytes src/main/omr/ui/resources/icon-32.png | Bin 0 -> 1170 bytes src/main/omr/ui/resources/icon-48.png | Bin 0 -> 1913 bytes src/main/omr/ui/symbol/Alignment.java | 398 +++ src/main/omr/ui/symbol/BackToBackSymbol.java | 132 + src/main/omr/ui/symbol/BasicSymbol.java | 514 +++ src/main/omr/ui/symbol/BeamHookSymbol.java | 73 + src/main/omr/ui/symbol/BeamSymbol.java | 171 + src/main/omr/ui/symbol/BraceSymbol.java | 113 + src/main/omr/ui/symbol/BracketSymbol.java | 121 + src/main/omr/ui/symbol/CrescendoSymbol.java | 115 + .../omr/ui/symbol/CustomNumDenSymbol.java | 120 + src/main/omr/ui/symbol/DecrescendoSymbol.java | 83 + .../omr/ui/symbol/DoubleBarlineSymbol.java | 96 + src/main/omr/ui/symbol/EndingSymbol.java | 96 + src/main/omr/ui/symbol/FlagsDownSymbol.java | 142 + src/main/omr/ui/symbol/FlagsUpSymbol.java | 69 + src/main/omr/ui/symbol/FlatSymbol.java | 82 + src/main/omr/ui/symbol/HeadsSymbol.java | 112 + src/main/omr/ui/symbol/KeyFlatSymbol.java | 53 + src/main/omr/ui/symbol/KeySharpSymbol.java | 53 + src/main/omr/ui/symbol/KeySymbol.java | 148 + src/main/omr/ui/symbol/LedgerSymbol.java | 139 + src/main/omr/ui/symbol/LongRestSymbol.java | 85 + src/main/omr/ui/symbol/MusicFont.java | 315 ++ .../omr/ui/symbol/NonDraggableSymbol.java | 107 + src/main/omr/ui/symbol/NumDenSymbol.java | 169 + src/main/omr/ui/symbol/OmrFont.java | 203 ++ src/main/omr/ui/symbol/OttavaClefSymbol.java | 127 + src/main/omr/ui/symbol/ResizedSymbol.java | 110 + src/main/omr/ui/symbol/RestSymbol.java | 168 + src/main/omr/ui/symbol/ShapeSymbol.java | 199 ++ src/main/omr/ui/symbol/SlantedSymbol.java | 213 ++ src/main/omr/ui/symbol/SlurSymbol.java | 106 + src/main/omr/ui/symbol/StemSymbol.java | 135 + src/main/omr/ui/symbol/Symbol.java | 90 + src/main/omr/ui/symbol/SymbolImage.java | 69 + src/main/omr/ui/symbol/SymbolPicture.java | 127 + src/main/omr/ui/symbol/SymbolRipper.java | 502 +++ src/main/omr/ui/symbol/Symbols.java | 479 +++ src/main/omr/ui/symbol/TextFont.java | 159 + src/main/omr/ui/symbol/TextSymbol.java | 95 + src/main/omr/ui/symbol/TransformedSymbol.java | 105 + src/main/omr/ui/symbol/TurnSlashSymbol.java | 109 + .../omr/ui/symbol/doc-files/KeySignatures.png | Bin 0 -> 906 bytes .../omr/ui/symbol/doc-files/SymbolRipper.png | Bin 0 -> 20367 bytes src/main/omr/ui/symbol/package.html | 16 + .../omr/ui/treetable/AbstractCellEditor.java | 172 + .../ui/treetable/AbstractTreeTableModel.java | 349 ++ src/main/omr/ui/treetable/JTreeTable.java | 485 +++ src/main/omr/ui/treetable/TreeTableModel.java | 83 + .../ui/treetable/TreeTableModelAdapter.java | 268 ++ src/main/omr/ui/treetable/package.html | 18 + src/main/omr/ui/util/DynamicMenu.java | 109 + src/main/omr/ui/util/ModelessOptionPane.java | 206 ++ src/main/omr/ui/util/OmrFileFilter.java | 148 + src/main/omr/ui/util/Panel.java | 385 +++ src/main/omr/ui/util/SeparableMenu.java | 107 + src/main/omr/ui/util/SeparablePopupMenu.java | 77 + src/main/omr/ui/util/SeparableToolBar.java | 131 + src/main/omr/ui/util/UILookAndFeel.java | 132 + src/main/omr/ui/util/UIPredicates.java | 147 + src/main/omr/ui/util/UIUtil.java | 317 ++ src/main/omr/ui/util/WebBrowser.java | 239 ++ src/main/omr/ui/util/package.html | 17 + src/main/omr/ui/view/LogSlider.java | 217 ++ src/main/omr/ui/view/MouseMonitor.java | 86 + src/main/omr/ui/view/PixelFocus.java | 42 + src/main/omr/ui/view/Rubber.java | 771 +++++ src/main/omr/ui/view/RubberPanel.java | 649 ++++ src/main/omr/ui/view/ScrollView.java | 292 ++ src/main/omr/ui/view/Zoom.java | 451 +++ src/main/omr/ui/view/package.html | 16 + src/main/omr/util/BasicTask.java | 39 + src/main/omr/util/BlackList.java | 262 ++ src/main/omr/util/BrokenLine.java | 521 +++ src/main/omr/util/ClassUtil.java | 212 ++ src/main/omr/util/Clock.java | 105 + src/main/omr/util/Concurrency.java | 31 + src/main/omr/util/DimensionFacade.java | 108 + src/main/omr/util/DoubleValue.java | 130 + src/main/omr/util/Dumper.java | 478 +++ src/main/omr/util/Dumping.java | 248 ++ src/main/omr/util/FileUtil.java | 155 + src/main/omr/util/GeoUtil.java | 98 + src/main/omr/util/HorizontalSide.java | 26 + src/main/omr/util/InstancesWatcher.java | 69 + src/main/omr/util/LiveParam.java | 79 + src/main/omr/util/Memory.java | 133 + src/main/omr/util/NameSet.java | 275 ++ src/main/omr/util/Navigable.java | 34 + src/main/omr/util/OmrExecutors.java | 459 +++ src/main/omr/util/Param.java | 143 + src/main/omr/util/PointFacade.java | 116 + src/main/omr/util/Predicate.java | 31 + src/main/omr/util/RectangleFacade.java | 144 + src/main/omr/util/RegexUtil.java | 78 + src/main/omr/util/StopWatch.java | 165 + src/main/omr/util/TreeNode.java | 280 ++ src/main/omr/util/UriUtil.java | 134 + src/main/omr/util/VerticalSide.java | 26 + src/main/omr/util/Vip.java | 36 + src/main/omr/util/VipUtil.java | 63 + .../omr/util/WeakPropertyChangeListener.java | 65 + src/main/omr/util/WindowsRegistry.java | 88 + src/main/omr/util/Worker.java | 242 ++ src/main/omr/util/WrappedBoolean.java | 64 + src/main/omr/util/Wrapper.java | 37 + src/main/omr/util/XmlUtil.java | 77 + src/main/omr/util/Zip.java | 180 + src/main/omr/util/package.html | 16 + src/main/overview.html | 126 + src/test/AudiverisTest.java | 89 + src/test/Tiny.java | 196 ++ src/test/omr/graph/GraphTest.java | 391 +++ src/test/omr/grid/FilamentTest.java | 534 +++ src/test/omr/jai/ImageInfo.java | 221 ++ src/test/omr/jai/RGBToBilevel.java | 129 + src/test/omr/jai/TestImage.java | 156 + src/test/omr/jai/TestImage2.java | 195 ++ src/test/omr/jai/TestImage3.java | 371 ++ src/test/omr/jai/TestWarp.java | 175 + src/test/omr/jai/TiffSplit.java | 77 + src/test/omr/jaxb/basic/BasicTest.java | 116 + src/test/omr/jaxb/basic/Day.java | 24 + src/test/omr/jaxb/basic/Meeting.java | 46 + src/test/omr/jaxb/basic/MyPoint.java | 76 + src/test/omr/jaxb/basic/Purse.java | 41 + src/test/omr/jaxb/basic/Waiter.java | 106 + src/test/omr/jaxb/basic/Weekday.java | 28 + src/test/omr/jaxb/basic/basic-data.xml | 27 + .../omr/jaxb/basic/basic-data.xml.out.xml | 44 + src/test/omr/jaxb/basic/basic.jibx.xml | 46 + src/test/omr/lag/LagTest.java | 718 ++++ src/test/omr/lag/SectionBindingTest.java | 211 ++ src/test/omr/math/BasicLineCheck.java | 479 +++ src/test/omr/math/BasicLineTest.java | 286 ++ src/test/omr/math/GeometricMomentsTest.java | 75 + src/test/omr/math/HistogramTest.java | 366 ++ src/test/omr/math/InjectionSolverTest.java | 72 + src/test/omr/math/IntegerHistogramTest.java | 72 + src/test/omr/math/LinearEvaluatorTest.java | 253 ++ src/test/omr/math/NaturalSplineTest.java | 211 ++ src/test/omr/math/NeuralNetworkTest.java | 336 ++ src/test/omr/math/PopulationTest.java | 181 + src/test/omr/math/RationalTest.java | 313 ++ src/test/omr/moment/ARTExtractorTest.java | 43 + src/test/omr/moment/LegendreMomentsTest.java | 44 + src/test/omr/moment/MomentsExtractorTest.java | 256 ++ src/test/omr/run/RunsTableTest.java | 346 ++ src/test/omr/score/entity/ChordInfoTest.java | 911 +++++ .../omr/sheet/picture/GhostscriptTest.java | 36 + src/test/omr/ui/BsafTest.java | 176 + src/test/omr/ui/MouseWheelTest.java | 154 + src/test/omr/ui/TestComponentEvent.java | 95 + src/test/omr/ui/ZoomTest.java | 101 + src/test/omr/ui/symbol/AlignmentTest.java | 153 + src/test/omr/ui/symbol/MusicFontTest.java | 151 + src/test/omr/util/BaseTestCase.java | 86 + src/test/omr/util/BrokenLineTest.java | 248 ++ src/test/omr/util/StopWatchTest.java | 94 + src/test/omr/util/TestUtilities.java | 112 + .../audiveris/musicxmldiff/BasicFilter.java | 171 + .../main/com/audiveris/musicxmldiff/CLI.java | 428 +++ .../com/audiveris/musicxmldiff/Filter.java | 86 + .../main/com/audiveris/musicxmldiff/Main.java | 229 ++ .../musicxmldiff/MusicDifferenceListener.java | 149 + .../audiveris/musicxmldiff/MusicPrinter.java | 135 + .../musicxmldiff/PositionalXMLReader.java | 261 ++ .../com/audiveris/musicxmldiff/Printer.java | 52 + .../com/audiveris/musicxmldiff/Tolerance.java | 147 + .../audiveris/musicxmldiff/info/AttrInfo.java | 43 + .../musicxmldiff/info/BasicInfo.java | 82 + .../audiveris/musicxmldiff/info/DiffInfo.java | 26 + .../audiveris/musicxmldiff/info/ElemInfo.java | 102 + .../musicxmldiff/info/FilterInfo.java | 336 ++ .../audiveris/musicxmldiff/info/NodeInfo.java | 85 + .../musicxmldiff/info/FilterInfoTest.java | 171 + src/tools/wixheatuser/Harvester.java | 436 +++ 805 files changed, 203220 insertions(+) create mode 100644 src/installer-test/com/audiveris/installer/unix/UnixUtilitiesTest.java create mode 100644 src/installer-test/com/audiveris/installer/unix/VersionNumberTest.java create mode 100644 src/installer/com/audiveris/installer/AbstractCompanion.java create mode 100644 src/installer/com/audiveris/installer/BasicCompanionView.java create mode 100644 src/installer/com/audiveris/installer/Bundle.java create mode 100644 src/installer/com/audiveris/installer/BundleView.java create mode 100644 src/installer/com/audiveris/installer/Companion.java create mode 100644 src/installer/com/audiveris/installer/CompanionView.java create mode 100644 src/installer/com/audiveris/installer/CppCompanion.java create mode 100644 src/installer/com/audiveris/installer/Descriptor.java create mode 100644 src/installer/com/audiveris/installer/DescriptorFactory.java create mode 100644 src/installer/com/audiveris/installer/DocCompanion.java create mode 100644 src/installer/com/audiveris/installer/ExamplesCompanion.java create mode 100644 src/installer/com/audiveris/installer/Expander.java create mode 100644 src/installer/com/audiveris/installer/FileCopier.java create mode 100644 src/installer/com/audiveris/installer/FolderSelector.java create mode 100644 src/installer/com/audiveris/installer/GhostscriptCompanion.java create mode 100644 src/installer/com/audiveris/installer/Installer.java create mode 100644 src/installer/com/audiveris/installer/JarExpander.java create mode 100644 src/installer/com/audiveris/installer/Jnlp.java create mode 100644 src/installer/com/audiveris/installer/JnlpResponseCache.java create mode 100644 src/installer/com/audiveris/installer/LangSelector.java create mode 100644 src/installer/com/audiveris/installer/LicenseCompanion.java create mode 100644 src/installer/com/audiveris/installer/LogUtilities.java create mode 100644 src/installer/com/audiveris/installer/MessagePanel.java create mode 100644 src/installer/com/audiveris/installer/OcrCompanion.java create mode 100644 src/installer/com/audiveris/installer/PluginsCompanion.java create mode 100644 src/installer/com/audiveris/installer/RegexUtil.java create mode 100644 src/installer/com/audiveris/installer/SpecificFile.java create mode 100644 src/installer/com/audiveris/installer/TrainingCompanion.java create mode 100644 src/installer/com/audiveris/installer/TreeRemover.java create mode 100644 src/installer/com/audiveris/installer/UnsupportedEnvironmentException.java create mode 100644 src/installer/com/audiveris/installer/Utilities.java create mode 100644 src/installer/com/audiveris/installer/ViewAppender.java create mode 100644 src/installer/com/audiveris/installer/mac/package.html create mode 100644 src/installer/com/audiveris/installer/package.html create mode 100644 src/installer/com/audiveris/installer/unix/Package.java create mode 100644 src/installer/com/audiveris/installer/unix/UnixDescriptor.java create mode 100644 src/installer/com/audiveris/installer/unix/UnixUtilities.java create mode 100644 src/installer/com/audiveris/installer/unix/VersionNumber.java create mode 100644 src/installer/com/audiveris/installer/unix/package.html create mode 100644 src/installer/com/audiveris/installer/windows/WindowsDescriptor.java create mode 100644 src/installer/com/audiveris/installer/windows/WindowsUtilities.java create mode 100644 src/installer/com/audiveris/installer/windows/package.html create mode 100644 src/installer/doc-files/overview.uxf create mode 100644 src/installer/doc-files/roles.uxf create mode 100644 src/installer/hudson/util/jna/InitializationErrorInvocationHandler.java create mode 100644 src/installer/hudson/util/jna/Kernel32.java create mode 100644 src/installer/hudson/util/jna/Kernel32Utils.java create mode 100644 src/installer/hudson/util/jna/SHELLEXECUTEINFO.java create mode 100644 src/installer/hudson/util/jna/Shell32.java create mode 100644 src/installer/hudson/util/jna/WinIOException.java create mode 100644 src/installer/hudson/util/jna/package.html create mode 100644 src/installer/overview.html create mode 100644 src/main/Audiveris.java create mode 100644 src/main/omr/CLI.java create mode 100644 src/main/omr/Debug.java create mode 100644 src/main/omr/Main.java create mode 100644 src/main/omr/WellKnowns.java create mode 100644 src/main/omr/action/ActionDescriptor.java create mode 100644 src/main/omr/action/ActionManager.java create mode 100644 src/main/omr/action/Actions.java create mode 100644 src/main/omr/action/package.html create mode 100644 src/main/omr/action/resources/Actions.properties create mode 100644 src/main/omr/action/resources/Actions_fr.properties create mode 100644 src/main/omr/check/Check.java create mode 100644 src/main/omr/check/CheckBoard.java create mode 100644 src/main/omr/check/CheckPanel.java create mode 100644 src/main/omr/check/CheckResult.java create mode 100644 src/main/omr/check/CheckSuite.java create mode 100644 src/main/omr/check/Checkable.java create mode 100644 src/main/omr/check/FailureResult.java create mode 100644 src/main/omr/check/Result.java create mode 100644 src/main/omr/check/SuccessResult.java create mode 100644 src/main/omr/check/package.html create mode 100644 src/main/omr/constant/Constant.java create mode 100644 src/main/omr/constant/ConstantManager.java create mode 100644 src/main/omr/constant/ConstantSet.java create mode 100644 src/main/omr/constant/Node.java create mode 100644 src/main/omr/constant/PackageNode.java create mode 100644 src/main/omr/constant/UnitManager.java create mode 100644 src/main/omr/constant/UnitModel.java create mode 100644 src/main/omr/constant/UnitNode.java create mode 100644 src/main/omr/constant/UnitTreeTable.java create mode 100644 src/main/omr/constant/doc-files/Constant.uxf create mode 100644 src/main/omr/constant/package.html create mode 100644 src/main/omr/glyph/AbstractEvaluationEngine.java create mode 100644 src/main/omr/glyph/BasicNest.java create mode 100644 src/main/omr/glyph/CompoundBuilder.java create mode 100644 src/main/omr/glyph/Evaluation.java create mode 100644 src/main/omr/glyph/EvaluationEngine.java create mode 100644 src/main/omr/glyph/GlyphInspector.java create mode 100644 src/main/omr/glyph/GlyphNetwork.java create mode 100644 src/main/omr/glyph/GlyphRegression.java create mode 100644 src/main/omr/glyph/GlyphRepository.java create mode 100644 src/main/omr/glyph/GlyphSignature.java create mode 100644 src/main/omr/glyph/Glyphs.java create mode 100644 src/main/omr/glyph/GlyphsBuilder.java create mode 100644 src/main/omr/glyph/GlyphsModel.java create mode 100644 src/main/omr/glyph/Grades.java create mode 100644 src/main/omr/glyph/Nest.java create mode 100644 src/main/omr/glyph/SectionSets.java create mode 100644 src/main/omr/glyph/Shape.java create mode 100644 src/main/omr/glyph/ShapeChecker.java create mode 100644 src/main/omr/glyph/ShapeDescription.java create mode 100644 src/main/omr/glyph/ShapeDescriptorART.java create mode 100644 src/main/omr/glyph/ShapeDescriptorGeo.java create mode 100644 src/main/omr/glyph/ShapeEvaluator.java create mode 100644 src/main/omr/glyph/ShapeSet.java create mode 100644 src/main/omr/glyph/SymbolGlyph.java create mode 100644 src/main/omr/glyph/SymbolGlyphDescriptor.java create mode 100644 src/main/omr/glyph/SymbolsModel.java create mode 100644 src/main/omr/glyph/VirtualGlyph.java create mode 100644 src/main/omr/glyph/doc-files/GlyphEvaluator.uxf create mode 100644 src/main/omr/glyph/doc-files/Glyphs.uxf create mode 100644 src/main/omr/glyph/facets/BasicAdministration.java create mode 100644 src/main/omr/glyph/facets/BasicAlignment.java create mode 100644 src/main/omr/glyph/facets/BasicComposition.java create mode 100644 src/main/omr/glyph/facets/BasicDisplay.java create mode 100644 src/main/omr/glyph/facets/BasicEnvironment.java create mode 100644 src/main/omr/glyph/facets/BasicFacet.java create mode 100644 src/main/omr/glyph/facets/BasicGeometry.java create mode 100644 src/main/omr/glyph/facets/BasicGlyph.java create mode 100644 src/main/omr/glyph/facets/BasicRecognition.java create mode 100644 src/main/omr/glyph/facets/BasicTranslation.java create mode 100644 src/main/omr/glyph/facets/Glyph.java create mode 100644 src/main/omr/glyph/facets/GlyphAdministration.java create mode 100644 src/main/omr/glyph/facets/GlyphAlignment.java create mode 100644 src/main/omr/glyph/facets/GlyphComposition.java create mode 100644 src/main/omr/glyph/facets/GlyphContent.java create mode 100644 src/main/omr/glyph/facets/GlyphDisplay.java create mode 100644 src/main/omr/glyph/facets/GlyphEnvironment.java create mode 100644 src/main/omr/glyph/facets/GlyphFacet.java create mode 100644 src/main/omr/glyph/facets/GlyphGeometry.java create mode 100644 src/main/omr/glyph/facets/GlyphRecognition.java create mode 100644 src/main/omr/glyph/facets/GlyphTranslation.java create mode 100644 src/main/omr/glyph/facets/GlyphValue.java create mode 100644 src/main/omr/glyph/facets/package.html create mode 100644 src/main/omr/glyph/package.html create mode 100644 src/main/omr/glyph/pattern/AlterPattern.java create mode 100644 src/main/omr/glyph/pattern/ArticulationPattern.java create mode 100644 src/main/omr/glyph/pattern/BassPattern.java create mode 100644 src/main/omr/glyph/pattern/BeamHookPattern.java create mode 100644 src/main/omr/glyph/pattern/CaesuraPattern.java create mode 100644 src/main/omr/glyph/pattern/ClefPattern.java create mode 100644 src/main/omr/glyph/pattern/DotPattern.java create mode 100644 src/main/omr/glyph/pattern/DoubleBeamPattern.java create mode 100644 src/main/omr/glyph/pattern/FermataDotPattern.java create mode 100644 src/main/omr/glyph/pattern/FlagPattern.java create mode 100644 src/main/omr/glyph/pattern/FortePattern.java create mode 100644 src/main/omr/glyph/pattern/GlyphPattern.java create mode 100644 src/main/omr/glyph/pattern/HiddenSlurPattern.java create mode 100644 src/main/omr/glyph/pattern/LedgerPattern.java create mode 100644 src/main/omr/glyph/pattern/LeftOverPattern.java create mode 100644 src/main/omr/glyph/pattern/PatternsChecker.java create mode 100644 src/main/omr/glyph/pattern/SlurInspector.java create mode 100644 src/main/omr/glyph/pattern/SplitPattern.java create mode 100644 src/main/omr/glyph/pattern/StemPattern.java create mode 100644 src/main/omr/glyph/pattern/TimePattern.java create mode 100644 src/main/omr/glyph/pattern/package.html create mode 100644 src/main/omr/glyph/ui/AttachmentHolder.java create mode 100644 src/main/omr/glyph/ui/BasicAttachmentHolder.java create mode 100644 src/main/omr/glyph/ui/EvaluationBoard.java create mode 100644 src/main/omr/glyph/ui/GlyphBoard.java create mode 100644 src/main/omr/glyph/ui/GlyphBrowser.java create mode 100644 src/main/omr/glyph/ui/GlyphMenu.java create mode 100644 src/main/omr/glyph/ui/GlyphsController.java create mode 100644 src/main/omr/glyph/ui/NestView.java create mode 100644 src/main/omr/glyph/ui/SampleVerifier.java create mode 100644 src/main/omr/glyph/ui/ShapeBoard.java create mode 100644 src/main/omr/glyph/ui/ShapeColorChooser.java create mode 100644 src/main/omr/glyph/ui/ShapeFocusBoard.java create mode 100644 src/main/omr/glyph/ui/SpinnerGlyphModel.java create mode 100644 src/main/omr/glyph/ui/SymbolGlyphBoard.java create mode 100644 src/main/omr/glyph/ui/SymbolMenu.java create mode 100644 src/main/omr/glyph/ui/SymbolsBlackList.java create mode 100644 src/main/omr/glyph/ui/SymbolsController.java create mode 100644 src/main/omr/glyph/ui/SymbolsEditor.java create mode 100644 src/main/omr/glyph/ui/UserEventSubscriber.java create mode 100644 src/main/omr/glyph/ui/ViewParameters.java create mode 100644 src/main/omr/glyph/ui/doc-files/GlyphsController.uxf create mode 100644 src/main/omr/glyph/ui/doc-files/SymbolGlyphBoard.png create mode 100644 src/main/omr/glyph/ui/package.html create mode 100644 src/main/omr/glyph/ui/panel/GlyphTrainer.java create mode 100644 src/main/omr/glyph/ui/panel/NetworkPanel.java create mode 100644 src/main/omr/glyph/ui/panel/RegressionPanel.java create mode 100644 src/main/omr/glyph/ui/panel/SelectionPanel.java create mode 100644 src/main/omr/glyph/ui/panel/TrainingPanel.java create mode 100644 src/main/omr/glyph/ui/panel/ValidationPanel.java create mode 100644 src/main/omr/glyph/ui/panel/package.html create mode 100644 src/main/omr/glyph/ui/panel/resources/GlyphTrainer.properties create mode 100644 src/main/omr/glyph/ui/panel/resources/GlyphTrainer_fr_FR.properties create mode 100644 src/main/omr/glyph/ui/resources/SampleVerifier.properties create mode 100644 src/main/omr/glyph/ui/resources/SampleVerifier_fr.properties create mode 100644 src/main/omr/glyph/ui/resources/ShapeColorChooser.properties create mode 100644 src/main/omr/glyph/ui/resources/ShapeColorChooser_fr.properties create mode 100644 src/main/omr/glyph/ui/resources/ViewParameters.properties create mode 100644 src/main/omr/glyph/ui/resources/ViewParameters_fr.properties create mode 100644 src/main/omr/graph/BasicDigraph.java create mode 100644 src/main/omr/graph/BasicVertex.java create mode 100644 src/main/omr/graph/Digraph.java create mode 100644 src/main/omr/graph/DigraphView.java create mode 100644 src/main/omr/graph/Vertex.java create mode 100644 src/main/omr/graph/VertexView.java create mode 100644 src/main/omr/graph/package.html create mode 100644 src/main/omr/grid/BarAlignment.java create mode 100644 src/main/omr/grid/BarInfo.java create mode 100644 src/main/omr/grid/BarsRetriever.java create mode 100644 src/main/omr/grid/ClustersRetriever.java create mode 100644 src/main/omr/grid/Filament.java create mode 100644 src/main/omr/grid/FilamentAlignment.java create mode 100644 src/main/omr/grid/FilamentComb.java create mode 100644 src/main/omr/grid/FilamentLine.java create mode 100644 src/main/omr/grid/FilamentsFactory.java create mode 100644 src/main/omr/grid/GridBuilder.java create mode 100644 src/main/omr/grid/IntersectionSequence.java create mode 100644 src/main/omr/grid/LagWeaver.java create mode 100644 src/main/omr/grid/LineCluster.java create mode 100644 src/main/omr/grid/LineFilament.java create mode 100644 src/main/omr/grid/LineFilamentAlignment.java create mode 100644 src/main/omr/grid/LineInfo.java create mode 100644 src/main/omr/grid/LinesRetriever.java create mode 100644 src/main/omr/grid/RunsViewer.java create mode 100644 src/main/omr/grid/StaffInfo.java create mode 100644 src/main/omr/grid/StaffManager.java create mode 100644 src/main/omr/grid/StickIntersection.java create mode 100644 src/main/omr/grid/TargetBuilder.java create mode 100644 src/main/omr/grid/TargetLine.java create mode 100644 src/main/omr/grid/TargetPage.java create mode 100644 src/main/omr/grid/TargetStaff.java create mode 100644 src/main/omr/grid/TargetSystem.java create mode 100644 src/main/omr/grid/doc-files/cluster.uxf create mode 100644 src/main/omr/grid/doc-files/filament.uxf create mode 100644 src/main/omr/grid/doc-files/grid.uxf create mode 100644 src/main/omr/grid/doc-files/pixel.uxf create mode 100644 src/main/omr/grid/doc-files/target.uxf create mode 100644 src/main/omr/grid/package.html create mode 100644 src/main/omr/lag/BasicLag.java create mode 100644 src/main/omr/lag/BasicRoi.java create mode 100644 src/main/omr/lag/BasicSection.java create mode 100644 src/main/omr/lag/JunctionAllPolicy.java create mode 100644 src/main/omr/lag/JunctionDeltaPolicy.java create mode 100644 src/main/omr/lag/JunctionPolicy.java create mode 100644 src/main/omr/lag/JunctionRatioPolicy.java create mode 100644 src/main/omr/lag/Lag.java create mode 100644 src/main/omr/lag/Roi.java create mode 100644 src/main/omr/lag/Section.java create mode 100644 src/main/omr/lag/SectionSignature.java create mode 100644 src/main/omr/lag/Sections.java create mode 100644 src/main/omr/lag/SectionsBuilder.java create mode 100644 src/main/omr/lag/package.html create mode 100644 src/main/omr/lag/ui/SectionBoard.java create mode 100644 src/main/omr/lag/ui/SectionView.java create mode 100644 src/main/omr/lag/ui/SpinnerSectionModel.java create mode 100644 src/main/omr/lag/ui/package.html create mode 100644 src/main/omr/log/LogGuiAppender.java create mode 100644 src/main/omr/log/LogPane.java create mode 100644 src/main/omr/log/LogStepAppender.java create mode 100644 src/main/omr/log/LogUtil.java create mode 100644 src/main/omr/log/LoggingStream.java create mode 100644 src/main/omr/log/package.html create mode 100644 src/main/omr/math/Barycenter.java create mode 100644 src/main/omr/math/BasicLine.java create mode 100644 src/main/omr/math/Circle.java create mode 100644 src/main/omr/math/Ellipse.java create mode 100644 src/main/omr/math/GCD.java create mode 100644 src/main/omr/math/GeoPath.java create mode 100644 src/main/omr/math/GeoUtil.java create mode 100644 src/main/omr/math/Histogram.java create mode 100644 src/main/omr/math/InjectionSolver.java create mode 100644 src/main/omr/math/IntegerHistogram.java create mode 100644 src/main/omr/math/Line.java create mode 100644 src/main/omr/math/LineUtil.java create mode 100644 src/main/omr/math/LinearEvaluator.java create mode 100644 src/main/omr/math/NaturalSpline.java create mode 100644 src/main/omr/math/NeuralNetwork.java create mode 100644 src/main/omr/math/PointsCollector.java create mode 100644 src/main/omr/math/Polynomial.java create mode 100644 src/main/omr/math/Population.java create mode 100644 src/main/omr/math/Rational.java create mode 100644 src/main/omr/math/ReversePathIterator.java create mode 100644 src/main/omr/math/package.html create mode 100644 src/main/omr/moments/ARTMoments.java create mode 100644 src/main/omr/moments/AbstractExtractor.java create mode 100644 src/main/omr/moments/BasicARTExtractor.java create mode 100644 src/main/omr/moments/BasicARTMoments.java create mode 100644 src/main/omr/moments/BasicLUT.java create mode 100644 src/main/omr/moments/BasicLegendreExtractor.java create mode 100644 src/main/omr/moments/BasicLegendreMoments.java create mode 100644 src/main/omr/moments/GeometricMoments.java create mode 100644 src/main/omr/moments/LUT.java create mode 100644 src/main/omr/moments/LegendreMoments.java create mode 100644 src/main/omr/moments/MomentsExtractor.java create mode 100644 src/main/omr/moments/OrthogonalMoments.java create mode 100644 src/main/omr/moments/QuantizedARTMoments.java create mode 100644 src/main/omr/moments/doc-files/moments.uxf create mode 100644 src/main/omr/moments/package.html create mode 100644 src/main/omr/package.html create mode 100644 src/main/omr/plugin/JavascriptUnavailableException.java create mode 100644 src/main/omr/plugin/Plugin.java create mode 100644 src/main/omr/plugin/PluginAction.java create mode 100644 src/main/omr/plugin/PluginsManager.java create mode 100644 src/main/omr/plugin/package.html create mode 100644 src/main/omr/plugin/resources/PluginsManager.properties create mode 100644 src/main/omr/plugin/resources/PluginsManager_fr.properties create mode 100644 src/main/omr/run/AdaptiveDescriptor.java create mode 100644 src/main/omr/run/AdaptiveFilter.java create mode 100644 src/main/omr/run/FilterDescriptor.java create mode 100644 src/main/omr/run/FilterKind.java create mode 100644 src/main/omr/run/GlobalDescriptor.java create mode 100644 src/main/omr/run/GlobalFilter.java create mode 100644 src/main/omr/run/Orientation.java create mode 100644 src/main/omr/run/Oriented.java create mode 100644 src/main/omr/run/PixelFilter.java create mode 100644 src/main/omr/run/PixelSource.java create mode 100644 src/main/omr/run/PixelsBuffer.java create mode 100644 src/main/omr/run/RandomFilter.java create mode 100644 src/main/omr/run/Run.java create mode 100644 src/main/omr/run/RunBoard.java create mode 100644 src/main/omr/run/RunsRetriever.java create mode 100644 src/main/omr/run/RunsTable.java create mode 100644 src/main/omr/run/RunsTableFactory.java create mode 100644 src/main/omr/run/RunsTableView.java create mode 100644 src/main/omr/run/SourceWrapper.java create mode 100644 src/main/omr/run/VerticalFilter.java create mode 100644 src/main/omr/run/package.html create mode 100644 src/main/omr/score/DurationRetriever.java create mode 100644 src/main/omr/score/KeySignatureVerifier.java create mode 100644 src/main/omr/score/MeasureBasicNumberer.java create mode 100644 src/main/omr/score/MeasureFixer.java create mode 100644 src/main/omr/score/MusicXML.java create mode 100644 src/main/omr/score/PageReduction.java create mode 100644 src/main/omr/score/PartConnection.java create mode 100644 src/main/omr/score/Score.java create mode 100644 src/main/omr/score/ScoreBench.java create mode 100644 src/main/omr/score/ScoreChecker.java create mode 100644 src/main/omr/score/ScoreCleaner.java create mode 100644 src/main/omr/score/ScoreExporter.java create mode 100644 src/main/omr/score/ScoreReduction.java create mode 100644 src/main/omr/score/ScoreReductor.java create mode 100644 src/main/omr/score/ScoreXmlReduction.java create mode 100644 src/main/omr/score/ScoresManager.java create mode 100644 src/main/omr/score/SlotBuilder.java create mode 100644 src/main/omr/score/SystemTranslator.java create mode 100644 src/main/omr/score/TimeSignatureFixer.java create mode 100644 src/main/omr/score/TimeSignatureRetriever.java create mode 100644 src/main/omr/score/doc-files/Part.uxf create mode 100644 src/main/omr/score/entity/AbstractDirection.java create mode 100644 src/main/omr/score/entity/AbstractNotation.java create mode 100644 src/main/omr/score/entity/Arpeggiate.java create mode 100644 src/main/omr/score/entity/Articulation.java create mode 100644 src/main/omr/score/entity/Barline.java create mode 100644 src/main/omr/score/entity/Beam.java create mode 100644 src/main/omr/score/entity/BeamGroup.java create mode 100644 src/main/omr/score/entity/BeamItem.java create mode 100644 src/main/omr/score/entity/Chord.java create mode 100644 src/main/omr/score/entity/ChordInfo.java create mode 100644 src/main/omr/score/entity/ChordSymbol.java create mode 100644 src/main/omr/score/entity/Clef.java create mode 100644 src/main/omr/score/entity/Coda.java create mode 100644 src/main/omr/score/entity/Container.java create mode 100644 src/main/omr/score/entity/Direction.java create mode 100644 src/main/omr/score/entity/DirectionStatement.java create mode 100644 src/main/omr/score/entity/DotTranslation.java create mode 100644 src/main/omr/score/entity/DurationFactor.java create mode 100644 src/main/omr/score/entity/Dynamics.java create mode 100644 src/main/omr/score/entity/Fermata.java create mode 100644 src/main/omr/score/entity/KeySignature.java create mode 100644 src/main/omr/score/entity/LyricsItem.java create mode 100644 src/main/omr/score/entity/LyricsLine.java create mode 100644 src/main/omr/score/entity/Mark.java create mode 100644 src/main/omr/score/entity/Measure.java create mode 100644 src/main/omr/score/entity/MeasureElement.java create mode 100644 src/main/omr/score/entity/MeasureId.java create mode 100644 src/main/omr/score/entity/MeasureNode.java create mode 100644 src/main/omr/score/entity/Notation.java create mode 100644 src/main/omr/score/entity/Note.java create mode 100644 src/main/omr/score/entity/Ornament.java create mode 100644 src/main/omr/score/entity/Page.java create mode 100644 src/main/omr/score/entity/PageNode.java create mode 100644 src/main/omr/score/entity/PartNode.java create mode 100644 src/main/omr/score/entity/Pedal.java create mode 100644 src/main/omr/score/entity/ScoreNode.java create mode 100644 src/main/omr/score/entity/ScorePart.java create mode 100644 src/main/omr/score/entity/ScoreSystem.java create mode 100644 src/main/omr/score/entity/Segno.java create mode 100644 src/main/omr/score/entity/Slot.java create mode 100644 src/main/omr/score/entity/Slur.java create mode 100644 src/main/omr/score/entity/Staff.java create mode 100644 src/main/omr/score/entity/SystemNode.java create mode 100644 src/main/omr/score/entity/SystemPart.java create mode 100644 src/main/omr/score/entity/Tempo.java create mode 100644 src/main/omr/score/entity/Text.java create mode 100644 src/main/omr/score/entity/TimeRational.java create mode 100644 src/main/omr/score/entity/TimeSignature.java create mode 100644 src/main/omr/score/entity/Tuplet.java create mode 100644 src/main/omr/score/entity/VisitableNode.java create mode 100644 src/main/omr/score/entity/Voice.java create mode 100644 src/main/omr/score/entity/Wedge.java create mode 100644 src/main/omr/score/entity/doc-files/Beam.uxf create mode 100644 src/main/omr/score/entity/doc-files/Note-Pack-both.png create mode 100644 src/main/omr/score/entity/doc-files/Note-Pack.png create mode 100644 src/main/omr/score/entity/doc-files/Slot.png create mode 100644 src/main/omr/score/entity/doc-files/TextTranslation.uxf create mode 100644 src/main/omr/score/entity/doc-files/Visitable-Hierarchy.uxf create mode 100644 src/main/omr/score/entity/package.html create mode 100644 src/main/omr/score/midi/MidiAbstractions.java create mode 100644 src/main/omr/score/midi/doc-files/midi.uxf create mode 100644 src/main/omr/score/midi/package.html create mode 100644 src/main/omr/score/package.html create mode 100644 src/main/omr/score/ui/BarPainter.java create mode 100644 src/main/omr/score/ui/PageMenu.java create mode 100644 src/main/omr/score/ui/PagePainter.java create mode 100644 src/main/omr/score/ui/PagePhysicalPainter.java create mode 100644 src/main/omr/score/ui/PaintingLayer.java create mode 100644 src/main/omr/score/ui/PaintingParameters.java create mode 100644 src/main/omr/score/ui/ScoreActions.java create mode 100644 src/main/omr/score/ui/ScoreController.java create mode 100644 src/main/omr/score/ui/ScoreDependent.java create mode 100644 src/main/omr/score/ui/ScoreParameters.java create mode 100644 src/main/omr/score/ui/ScoreTree.java create mode 100644 src/main/omr/score/ui/SheetPdfOutput.java create mode 100644 src/main/omr/score/ui/doc-files/ScoreParameters.png create mode 100644 src/main/omr/score/ui/doc-files/ScoreView.uxf create mode 100644 src/main/omr/score/ui/package.html create mode 100644 src/main/omr/score/ui/resources/PaintingParameters.properties create mode 100644 src/main/omr/score/ui/resources/PaintingParameters_fr.properties create mode 100644 src/main/omr/score/ui/resources/ScoreActions.properties create mode 100644 src/main/omr/score/ui/resources/ScoreActions_fr.properties create mode 100644 src/main/omr/score/ui/resources/ScoreTree.properties create mode 100644 src/main/omr/score/ui/resources/ScoreTree_fr.properties create mode 100644 src/main/omr/score/visitor/AbstractScoreVisitor.java create mode 100644 src/main/omr/score/visitor/ScoreVisitor.java create mode 100644 src/main/omr/score/visitor/Visitable.java create mode 100644 src/main/omr/score/visitor/package.html create mode 100644 src/main/omr/script/AssignTask.java create mode 100644 src/main/omr/script/BarlineTask.java create mode 100644 src/main/omr/script/BoundaryTask.java create mode 100644 src/main/omr/script/DeleteTask.java create mode 100644 src/main/omr/script/ExportTask.java create mode 100644 src/main/omr/script/GlyphTask.java create mode 100644 src/main/omr/script/GlyphUpdateTask.java create mode 100644 src/main/omr/script/InsertTask.java create mode 100644 src/main/omr/script/ParametersTask.java create mode 100644 src/main/omr/script/PrintTask.java create mode 100644 src/main/omr/script/RationalTask.java create mode 100644 src/main/omr/script/RemoveTask.java create mode 100644 src/main/omr/script/Script.java create mode 100644 src/main/omr/script/ScriptActions.java create mode 100644 src/main/omr/script/ScriptManager.java create mode 100644 src/main/omr/script/ScriptTask.java create mode 100644 src/main/omr/script/SegmentTask.java create mode 100644 src/main/omr/script/SheetTask.java create mode 100644 src/main/omr/script/SlurTask.java create mode 100644 src/main/omr/script/StepTask.java create mode 100644 src/main/omr/script/TextTask.java create mode 100644 src/main/omr/script/doc-files/script.uxf create mode 100644 src/main/omr/script/package.html create mode 100644 src/main/omr/script/resources/ScriptActions.properties create mode 100644 src/main/omr/script/resources/ScriptActions_fr.properties create mode 100644 src/main/omr/selection/GlyphEvent.java create mode 100644 src/main/omr/selection/GlyphIdEvent.java create mode 100644 src/main/omr/selection/GlyphSetEvent.java create mode 100644 src/main/omr/selection/LagEvent.java create mode 100644 src/main/omr/selection/LocationEvent.java create mode 100644 src/main/omr/selection/MouseMovement.java create mode 100644 src/main/omr/selection/NestEvent.java create mode 100644 src/main/omr/selection/PixelLevelEvent.java create mode 100644 src/main/omr/selection/RunEvent.java create mode 100644 src/main/omr/selection/SectionEvent.java create mode 100644 src/main/omr/selection/SectionIdEvent.java create mode 100644 src/main/omr/selection/SectionSetEvent.java create mode 100644 src/main/omr/selection/SelectionHint.java create mode 100644 src/main/omr/selection/SelectionService.java create mode 100644 src/main/omr/selection/SheetEvent.java create mode 100644 src/main/omr/selection/UserEvent.java create mode 100644 src/main/omr/selection/doc-files/Events.uxf create mode 100644 src/main/omr/selection/doc-files/Flows.uxf create mode 100644 src/main/omr/selection/package.html create mode 100644 src/main/omr/sheet/BarsChecker.java create mode 100644 src/main/omr/sheet/Bench.java create mode 100644 src/main/omr/sheet/BorderBuilder.java create mode 100644 src/main/omr/sheet/BrokenLineContext.java create mode 100644 src/main/omr/sheet/Dash.java create mode 100644 src/main/omr/sheet/Ending.java create mode 100644 src/main/omr/sheet/Horizontals.java create mode 100644 src/main/omr/sheet/HorizontalsBuilder.java create mode 100644 src/main/omr/sheet/Ledger.java create mode 100644 src/main/omr/sheet/MeasuresBuilder.java create mode 100644 src/main/omr/sheet/NotePosition.java create mode 100644 src/main/omr/sheet/PartInfo.java create mode 100644 src/main/omr/sheet/Scale.java create mode 100644 src/main/omr/sheet/ScaleBuilder.java create mode 100644 src/main/omr/sheet/Sheet.java create mode 100644 src/main/omr/sheet/SheetBench.java create mode 100644 src/main/omr/sheet/Skew.java create mode 100644 src/main/omr/sheet/SystemBoundary.java create mode 100644 src/main/omr/sheet/SystemInfo.java create mode 100644 src/main/omr/sheet/SystemsBuilder.java create mode 100644 src/main/omr/sheet/VerticalsBuilder.java create mode 100644 src/main/omr/sheet/VerticalsController.java create mode 100644 src/main/omr/sheet/package.html create mode 100644 src/main/omr/sheet/picture/Ghostscript.java create mode 100644 src/main/omr/sheet/picture/ImageFormatException.java create mode 100644 src/main/omr/sheet/picture/Picture.java create mode 100644 src/main/omr/sheet/picture/PictureLoader.java create mode 100644 src/main/omr/sheet/picture/PictureView.java create mode 100644 src/main/omr/sheet/picture/jai/JaiDewarper.java create mode 100644 src/main/omr/sheet/picture/jai/JaiLoader.java create mode 100644 src/main/omr/sheet/picture/package.html create mode 100644 src/main/omr/sheet/resources/LedgerParameters.properties create mode 100644 src/main/omr/sheet/resources/LedgerParameters_fr.properties create mode 100644 src/main/omr/sheet/resources/LinesParameters.properties create mode 100644 src/main/omr/sheet/resources/LinesParameters_fr.properties create mode 100644 src/main/omr/sheet/ui/BinarizationBoard.java create mode 100644 src/main/omr/sheet/ui/BoundaryEditor.java create mode 100644 src/main/omr/sheet/ui/PixelBoard.java create mode 100644 src/main/omr/sheet/ui/ScoreColorizer.java create mode 100644 src/main/omr/sheet/ui/SheetActions.java create mode 100644 src/main/omr/sheet/ui/SheetAssembly.java create mode 100644 src/main/omr/sheet/ui/SheetDependent.java create mode 100644 src/main/omr/sheet/ui/SheetPainter.java create mode 100644 src/main/omr/sheet/ui/SheetsController.java create mode 100644 src/main/omr/sheet/ui/package.html create mode 100644 src/main/omr/sheet/ui/resources/SheetActions.properties create mode 100644 src/main/omr/sheet/ui/resources/SheetActions_fr.properties create mode 100644 src/main/omr/step/AbstractStep.java create mode 100644 src/main/omr/step/AbstractSystemStep.java create mode 100644 src/main/omr/step/ExportStep.java create mode 100644 src/main/omr/step/GridStep.java create mode 100644 src/main/omr/step/LoadStep.java create mode 100644 src/main/omr/step/MeasuresStep.java create mode 100644 src/main/omr/step/PagesStep.java create mode 100644 src/main/omr/step/PluginStep.java create mode 100644 src/main/omr/step/PrintStep.java create mode 100644 src/main/omr/step/ProcessingCancellationException.java create mode 100644 src/main/omr/step/ScaleStep.java create mode 100644 src/main/omr/step/ScoreStep.java create mode 100644 src/main/omr/step/SheetTask.java create mode 100644 src/main/omr/step/Step.java create mode 100644 src/main/omr/step/StepException.java create mode 100644 src/main/omr/step/StepMenu.java create mode 100644 src/main/omr/step/StepMonitor.java create mode 100644 src/main/omr/step/Stepping.java create mode 100644 src/main/omr/step/Steps.java create mode 100644 src/main/omr/step/SticksStep.java create mode 100644 src/main/omr/step/SymbolsStep.java create mode 100644 src/main/omr/step/SystemsStep.java create mode 100644 src/main/omr/step/TextsStep.java create mode 100644 src/main/omr/step/doc-files/Step.uxf create mode 100644 src/main/omr/step/package.html create mode 100644 src/main/omr/stick/SectionRole.java create mode 100644 src/main/omr/stick/SectionsSource.java create mode 100644 src/main/omr/stick/StickRelation.java create mode 100644 src/main/omr/stick/SticksBuilder.java create mode 100644 src/main/omr/stick/UnknownSectionPredicate.java create mode 100644 src/main/omr/stick/package.html create mode 100644 src/main/omr/text/BasicContent.java create mode 100644 src/main/omr/text/FontInfo.java create mode 100644 src/main/omr/text/Language.java create mode 100644 src/main/omr/text/OCR.java create mode 100644 src/main/omr/text/TextBasedItem.java create mode 100644 src/main/omr/text/TextBuilder.java create mode 100644 src/main/omr/text/TextChar.java create mode 100644 src/main/omr/text/TextCheckerPattern.java create mode 100644 src/main/omr/text/TextItem.java create mode 100644 src/main/omr/text/TextLine.java create mode 100644 src/main/omr/text/TextPattern.java create mode 100644 src/main/omr/text/TextRole.java create mode 100644 src/main/omr/text/TextRoleInfo.java create mode 100644 src/main/omr/text/TextScanner.java create mode 100644 src/main/omr/text/TextWord.java create mode 100644 src/main/omr/text/WordScanner.java create mode 100644 src/main/omr/text/doc-files/Text Processing.uxf create mode 100644 src/main/omr/text/doc-files/Text.uxf create mode 100644 src/main/omr/text/package.html create mode 100644 src/main/omr/text/tesseract/TesseractOCR.java create mode 100644 src/main/omr/text/tesseract/TesseractOrder.java create mode 100644 src/main/omr/text/tesseract/package.html create mode 100644 src/main/omr/ui/Board.java create mode 100644 src/main/omr/ui/BoardsPane.java create mode 100644 src/main/omr/ui/Colors.java create mode 100644 src/main/omr/ui/EntityAction.java create mode 100644 src/main/omr/ui/ErrorsEditor.java create mode 100644 src/main/omr/ui/FileDropHandler.java create mode 100644 src/main/omr/ui/GuiActions.java create mode 100644 src/main/omr/ui/MacApplication.java create mode 100644 src/main/omr/ui/MainGui.java create mode 100644 src/main/omr/ui/MemoryMeter.java create mode 100644 src/main/omr/ui/OmrUIDefaults.java create mode 100644 src/main/omr/ui/Options.java create mode 100644 src/main/omr/ui/PixelCount.java create mode 100644 src/main/omr/ui/dnd/AbstractGhostDropListener.java create mode 100644 src/main/omr/ui/dnd/GhostComponentAdapter.java create mode 100644 src/main/omr/ui/dnd/GhostDropAdapter.java create mode 100644 src/main/omr/ui/dnd/GhostDropEvent.java create mode 100644 src/main/omr/ui/dnd/GhostDropListener.java create mode 100644 src/main/omr/ui/dnd/GhostGlassPane.java create mode 100644 src/main/omr/ui/dnd/GhostImageAdapter.java create mode 100644 src/main/omr/ui/dnd/GhostMotionAdapter.java create mode 100644 src/main/omr/ui/dnd/GhostPictureAdapter.java create mode 100644 src/main/omr/ui/dnd/ScreenPoint.java create mode 100644 src/main/omr/ui/dnd/doc-files/DnD.uxf create mode 100644 src/main/omr/ui/dnd/package.html create mode 100644 src/main/omr/ui/field/IntegerListSpinner.java create mode 100644 src/main/omr/ui/field/LComboBox.java create mode 100644 src/main/omr/ui/field/LDoubleField.java create mode 100644 src/main/omr/ui/field/LField.java create mode 100644 src/main/omr/ui/field/LHexaSpinner.java create mode 100644 src/main/omr/ui/field/LIntegerField.java create mode 100644 src/main/omr/ui/field/LIntegerSpinner.java create mode 100644 src/main/omr/ui/field/LSpinner.java create mode 100644 src/main/omr/ui/field/LTextField.java create mode 100644 src/main/omr/ui/field/SpinnerUtil.java create mode 100644 src/main/omr/ui/field/doc-files/Fields.uxf create mode 100644 src/main/omr/ui/field/package.html create mode 100644 src/main/omr/ui/package.html create mode 100644 src/main/omr/ui/resources/GuiActions.properties create mode 100644 src/main/omr/ui/resources/GuiActions_fr.properties create mode 100644 src/main/omr/ui/resources/MainGui.properties create mode 100644 src/main/omr/ui/resources/MainGui_fr.properties create mode 100644 src/main/omr/ui/resources/MemoryMeter.properties create mode 100644 src/main/omr/ui/resources/Options.properties create mode 100644 src/main/omr/ui/resources/Options_fr_FR.properties create mode 100644 src/main/omr/ui/resources/icon-16.png create mode 100644 src/main/omr/ui/resources/icon-24.png create mode 100644 src/main/omr/ui/resources/icon-256.png create mode 100644 src/main/omr/ui/resources/icon-32.png create mode 100644 src/main/omr/ui/resources/icon-48.png create mode 100644 src/main/omr/ui/symbol/Alignment.java create mode 100644 src/main/omr/ui/symbol/BackToBackSymbol.java create mode 100644 src/main/omr/ui/symbol/BasicSymbol.java create mode 100644 src/main/omr/ui/symbol/BeamHookSymbol.java create mode 100644 src/main/omr/ui/symbol/BeamSymbol.java create mode 100644 src/main/omr/ui/symbol/BraceSymbol.java create mode 100644 src/main/omr/ui/symbol/BracketSymbol.java create mode 100644 src/main/omr/ui/symbol/CrescendoSymbol.java create mode 100644 src/main/omr/ui/symbol/CustomNumDenSymbol.java create mode 100644 src/main/omr/ui/symbol/DecrescendoSymbol.java create mode 100644 src/main/omr/ui/symbol/DoubleBarlineSymbol.java create mode 100644 src/main/omr/ui/symbol/EndingSymbol.java create mode 100644 src/main/omr/ui/symbol/FlagsDownSymbol.java create mode 100644 src/main/omr/ui/symbol/FlagsUpSymbol.java create mode 100644 src/main/omr/ui/symbol/FlatSymbol.java create mode 100644 src/main/omr/ui/symbol/HeadsSymbol.java create mode 100644 src/main/omr/ui/symbol/KeyFlatSymbol.java create mode 100644 src/main/omr/ui/symbol/KeySharpSymbol.java create mode 100644 src/main/omr/ui/symbol/KeySymbol.java create mode 100644 src/main/omr/ui/symbol/LedgerSymbol.java create mode 100644 src/main/omr/ui/symbol/LongRestSymbol.java create mode 100644 src/main/omr/ui/symbol/MusicFont.java create mode 100644 src/main/omr/ui/symbol/NonDraggableSymbol.java create mode 100644 src/main/omr/ui/symbol/NumDenSymbol.java create mode 100644 src/main/omr/ui/symbol/OmrFont.java create mode 100644 src/main/omr/ui/symbol/OttavaClefSymbol.java create mode 100644 src/main/omr/ui/symbol/ResizedSymbol.java create mode 100644 src/main/omr/ui/symbol/RestSymbol.java create mode 100644 src/main/omr/ui/symbol/ShapeSymbol.java create mode 100644 src/main/omr/ui/symbol/SlantedSymbol.java create mode 100644 src/main/omr/ui/symbol/SlurSymbol.java create mode 100644 src/main/omr/ui/symbol/StemSymbol.java create mode 100644 src/main/omr/ui/symbol/Symbol.java create mode 100644 src/main/omr/ui/symbol/SymbolImage.java create mode 100644 src/main/omr/ui/symbol/SymbolPicture.java create mode 100644 src/main/omr/ui/symbol/SymbolRipper.java create mode 100644 src/main/omr/ui/symbol/Symbols.java create mode 100644 src/main/omr/ui/symbol/TextFont.java create mode 100644 src/main/omr/ui/symbol/TextSymbol.java create mode 100644 src/main/omr/ui/symbol/TransformedSymbol.java create mode 100644 src/main/omr/ui/symbol/TurnSlashSymbol.java create mode 100644 src/main/omr/ui/symbol/doc-files/KeySignatures.png create mode 100644 src/main/omr/ui/symbol/doc-files/SymbolRipper.png create mode 100644 src/main/omr/ui/symbol/package.html create mode 100644 src/main/omr/ui/treetable/AbstractCellEditor.java create mode 100644 src/main/omr/ui/treetable/AbstractTreeTableModel.java create mode 100644 src/main/omr/ui/treetable/JTreeTable.java create mode 100644 src/main/omr/ui/treetable/TreeTableModel.java create mode 100644 src/main/omr/ui/treetable/TreeTableModelAdapter.java create mode 100644 src/main/omr/ui/treetable/package.html create mode 100644 src/main/omr/ui/util/DynamicMenu.java create mode 100644 src/main/omr/ui/util/ModelessOptionPane.java create mode 100644 src/main/omr/ui/util/OmrFileFilter.java create mode 100644 src/main/omr/ui/util/Panel.java create mode 100644 src/main/omr/ui/util/SeparableMenu.java create mode 100644 src/main/omr/ui/util/SeparablePopupMenu.java create mode 100644 src/main/omr/ui/util/SeparableToolBar.java create mode 100644 src/main/omr/ui/util/UILookAndFeel.java create mode 100644 src/main/omr/ui/util/UIPredicates.java create mode 100644 src/main/omr/ui/util/UIUtil.java create mode 100644 src/main/omr/ui/util/WebBrowser.java create mode 100644 src/main/omr/ui/util/package.html create mode 100644 src/main/omr/ui/view/LogSlider.java create mode 100644 src/main/omr/ui/view/MouseMonitor.java create mode 100644 src/main/omr/ui/view/PixelFocus.java create mode 100644 src/main/omr/ui/view/Rubber.java create mode 100644 src/main/omr/ui/view/RubberPanel.java create mode 100644 src/main/omr/ui/view/ScrollView.java create mode 100644 src/main/omr/ui/view/Zoom.java create mode 100644 src/main/omr/ui/view/package.html create mode 100644 src/main/omr/util/BasicTask.java create mode 100644 src/main/omr/util/BlackList.java create mode 100644 src/main/omr/util/BrokenLine.java create mode 100644 src/main/omr/util/ClassUtil.java create mode 100644 src/main/omr/util/Clock.java create mode 100644 src/main/omr/util/Concurrency.java create mode 100644 src/main/omr/util/DimensionFacade.java create mode 100644 src/main/omr/util/DoubleValue.java create mode 100644 src/main/omr/util/Dumper.java create mode 100644 src/main/omr/util/Dumping.java create mode 100644 src/main/omr/util/FileUtil.java create mode 100644 src/main/omr/util/GeoUtil.java create mode 100644 src/main/omr/util/HorizontalSide.java create mode 100644 src/main/omr/util/InstancesWatcher.java create mode 100644 src/main/omr/util/LiveParam.java create mode 100644 src/main/omr/util/Memory.java create mode 100644 src/main/omr/util/NameSet.java create mode 100644 src/main/omr/util/Navigable.java create mode 100644 src/main/omr/util/OmrExecutors.java create mode 100644 src/main/omr/util/Param.java create mode 100644 src/main/omr/util/PointFacade.java create mode 100644 src/main/omr/util/Predicate.java create mode 100644 src/main/omr/util/RectangleFacade.java create mode 100644 src/main/omr/util/RegexUtil.java create mode 100644 src/main/omr/util/StopWatch.java create mode 100644 src/main/omr/util/TreeNode.java create mode 100644 src/main/omr/util/UriUtil.java create mode 100644 src/main/omr/util/VerticalSide.java create mode 100644 src/main/omr/util/Vip.java create mode 100644 src/main/omr/util/VipUtil.java create mode 100644 src/main/omr/util/WeakPropertyChangeListener.java create mode 100644 src/main/omr/util/WindowsRegistry.java create mode 100644 src/main/omr/util/Worker.java create mode 100644 src/main/omr/util/WrappedBoolean.java create mode 100644 src/main/omr/util/Wrapper.java create mode 100644 src/main/omr/util/XmlUtil.java create mode 100644 src/main/omr/util/Zip.java create mode 100644 src/main/omr/util/package.html create mode 100644 src/main/overview.html create mode 100644 src/test/AudiverisTest.java create mode 100644 src/test/Tiny.java create mode 100644 src/test/omr/graph/GraphTest.java create mode 100644 src/test/omr/grid/FilamentTest.java create mode 100644 src/test/omr/jai/ImageInfo.java create mode 100644 src/test/omr/jai/RGBToBilevel.java create mode 100644 src/test/omr/jai/TestImage.java create mode 100644 src/test/omr/jai/TestImage2.java create mode 100644 src/test/omr/jai/TestImage3.java create mode 100644 src/test/omr/jai/TestWarp.java create mode 100644 src/test/omr/jai/TiffSplit.java create mode 100644 src/test/omr/jaxb/basic/BasicTest.java create mode 100644 src/test/omr/jaxb/basic/Day.java create mode 100644 src/test/omr/jaxb/basic/Meeting.java create mode 100644 src/test/omr/jaxb/basic/MyPoint.java create mode 100644 src/test/omr/jaxb/basic/Purse.java create mode 100644 src/test/omr/jaxb/basic/Waiter.java create mode 100644 src/test/omr/jaxb/basic/Weekday.java create mode 100644 src/test/omr/jaxb/basic/basic-data.xml create mode 100644 src/test/omr/jaxb/basic/basic-data.xml.out.xml create mode 100644 src/test/omr/jaxb/basic/basic.jibx.xml create mode 100644 src/test/omr/lag/LagTest.java create mode 100644 src/test/omr/lag/SectionBindingTest.java create mode 100644 src/test/omr/math/BasicLineCheck.java create mode 100644 src/test/omr/math/BasicLineTest.java create mode 100644 src/test/omr/math/GeometricMomentsTest.java create mode 100644 src/test/omr/math/HistogramTest.java create mode 100644 src/test/omr/math/InjectionSolverTest.java create mode 100644 src/test/omr/math/IntegerHistogramTest.java create mode 100644 src/test/omr/math/LinearEvaluatorTest.java create mode 100644 src/test/omr/math/NaturalSplineTest.java create mode 100644 src/test/omr/math/NeuralNetworkTest.java create mode 100644 src/test/omr/math/PopulationTest.java create mode 100644 src/test/omr/math/RationalTest.java create mode 100644 src/test/omr/moment/ARTExtractorTest.java create mode 100644 src/test/omr/moment/LegendreMomentsTest.java create mode 100644 src/test/omr/moment/MomentsExtractorTest.java create mode 100644 src/test/omr/run/RunsTableTest.java create mode 100644 src/test/omr/score/entity/ChordInfoTest.java create mode 100644 src/test/omr/sheet/picture/GhostscriptTest.java create mode 100644 src/test/omr/ui/BsafTest.java create mode 100644 src/test/omr/ui/MouseWheelTest.java create mode 100644 src/test/omr/ui/TestComponentEvent.java create mode 100644 src/test/omr/ui/ZoomTest.java create mode 100644 src/test/omr/ui/symbol/AlignmentTest.java create mode 100644 src/test/omr/ui/symbol/MusicFontTest.java create mode 100644 src/test/omr/util/BaseTestCase.java create mode 100644 src/test/omr/util/BrokenLineTest.java create mode 100644 src/test/omr/util/StopWatchTest.java create mode 100644 src/test/omr/util/TestUtilities.java create mode 100644 src/tools/musicxmldiff/main/com/audiveris/musicxmldiff/BasicFilter.java create mode 100644 src/tools/musicxmldiff/main/com/audiveris/musicxmldiff/CLI.java create mode 100644 src/tools/musicxmldiff/main/com/audiveris/musicxmldiff/Filter.java create mode 100644 src/tools/musicxmldiff/main/com/audiveris/musicxmldiff/Main.java create mode 100644 src/tools/musicxmldiff/main/com/audiveris/musicxmldiff/MusicDifferenceListener.java create mode 100644 src/tools/musicxmldiff/main/com/audiveris/musicxmldiff/MusicPrinter.java create mode 100644 src/tools/musicxmldiff/main/com/audiveris/musicxmldiff/PositionalXMLReader.java create mode 100644 src/tools/musicxmldiff/main/com/audiveris/musicxmldiff/Printer.java create mode 100644 src/tools/musicxmldiff/main/com/audiveris/musicxmldiff/Tolerance.java create mode 100644 src/tools/musicxmldiff/main/com/audiveris/musicxmldiff/info/AttrInfo.java create mode 100644 src/tools/musicxmldiff/main/com/audiveris/musicxmldiff/info/BasicInfo.java create mode 100644 src/tools/musicxmldiff/main/com/audiveris/musicxmldiff/info/DiffInfo.java create mode 100644 src/tools/musicxmldiff/main/com/audiveris/musicxmldiff/info/ElemInfo.java create mode 100644 src/tools/musicxmldiff/main/com/audiveris/musicxmldiff/info/FilterInfo.java create mode 100644 src/tools/musicxmldiff/main/com/audiveris/musicxmldiff/info/NodeInfo.java create mode 100644 src/tools/musicxmldiff/test/com/audiveris/musicxmldiff/info/FilterInfoTest.java create mode 100644 src/tools/wixheatuser/Harvester.java diff --git a/src/installer-test/com/audiveris/installer/unix/UnixUtilitiesTest.java b/src/installer-test/com/audiveris/installer/unix/UnixUtilitiesTest.java new file mode 100644 index 0000000..699b04b --- /dev/null +++ b/src/installer-test/com/audiveris/installer/unix/UnixUtilitiesTest.java @@ -0,0 +1,54 @@ +//----------------------------------------------------------------------------// +// // +// U n i x U t i l i t i e s T e s t // +// // +//----------------------------------------------------------------------------// +// // +// Copyright © Herve 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. // +//----------------------------------------------------------------------------// +// +package com.audiveris.installer.unix; + +import com.audiveris.installer.DescriptorFactory; +import static org.junit.Assert.*; +import org.junit.Test; + +/** + * + * @author herve + */ +public class UnixUtilitiesTest +{ + //~ Methods ---------------------------------------------------------------- + + /** + * Test of getCommandLine method, of class UnixUtilities. + */ + @Test + public void testGetCommandLine () + { + if (DescriptorFactory.LINUX) { + System.out.println("getCommandLine"); + + String result = UnixUtilities.getCommandLine(); + System.out.println("result = " + result); + } + } + + /** + * Test of getPid method, of class UnixUtilities. + */ + @Test + public void testGetPid () + throws Exception + { + if (DescriptorFactory.LINUX) { + System.out.println("getPid"); + + String result = UnixUtilities.getPid(); + System.out.println("result = " + result); + } + } +} diff --git a/src/installer-test/com/audiveris/installer/unix/VersionNumberTest.java b/src/installer-test/com/audiveris/installer/unix/VersionNumberTest.java new file mode 100644 index 0000000..adcd863 --- /dev/null +++ b/src/installer-test/com/audiveris/installer/unix/VersionNumberTest.java @@ -0,0 +1,70 @@ +//----------------------------------------------------------------------------// +// // +// V e r s i o n N u m b e r T e s t // +// // +//----------------------------------------------------------------------------// +// // +// Copyright © Herve 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. // +//----------------------------------------------------------------------------// +// +package com.audiveris.installer.unix; + +import static org.junit.Assert.*; +import org.junit.Test; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Unitary tests for VersionNumber. + * + * @author Hervé Bitteur + */ +public class VersionNumberTest +{ + //~ Static fields/initializers --------------------------------------------- + + private static final Logger logger = LoggerFactory.getLogger( + VersionNumberTest.class); + + //~ Methods ---------------------------------------------------------------- + /** + * Test of compareTo method, of class VersionNumber. + */ + @Test + public void test_compareTo () + { + System.out.println("test_compareTo"); + + test("1.2.3", "2.3", -1); + test("1.2.3", "1.2.3", 0); + test("1.2.3-57", "1.2.3", 1); + test("1.2.3-57", "1.2.3-25alpha", 1); + test("1:4.7.2-2ubuntu1", "4.7.3", 1); + test("1:4.7.2-2ubuntu1", "2:0", -1); + test("1-~~", "1-~~a", -1); + test("2-~~a", "2-~", -1); + test("3~", "3", -1); + test("4", "4a", -1); + test("9.06~dfsg-0ubuntu4", "9.06", -1); + test("7u9-2.3.4-0ubuntu1.12.10.1", "9.06", -1); + } + + //------// + // test // + //------// + private void test (String v1, + String v2, + int exp) + { + logger.info("Test {} vs {}, exp:{}", v1, v2, exp); + + VersionNumber vn1 = new VersionNumber(v1); + VersionNumber vn2 = new VersionNumber(v2); + int res = vn1.compareTo(vn2); + logger.info("Result:{}", res); + assertEquals(exp, res); + } +} diff --git a/src/installer/com/audiveris/installer/AbstractCompanion.java b/src/installer/com/audiveris/installer/AbstractCompanion.java new file mode 100644 index 0000000..08fa526 --- /dev/null +++ b/src/installer/com/audiveris/installer/AbstractCompanion.java @@ -0,0 +1,352 @@ +//----------------------------------------------------------------------------// +// // +// A b s t r a c t C o m p a n i o n // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package com.audiveris.installer; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; + +/** + * Class {@code AbstractCompanion} is a basis for Companion + * implementations. + * + * @author Hervé Bitteur + */ +public abstract class AbstractCompanion + implements Companion +{ + //~ Static fields/initializers --------------------------------------------- + + /** Usual logger utility */ + private static final Logger logger = LoggerFactory.getLogger( + AbstractCompanion.class); + + /** To display a serial number for each companion. */ + private static int globalIndex = 0; + + //~ Instance fields -------------------------------------------------------- + /** Companion ID. */ + protected final int index; + + /** Companion title. */ + protected final String title; + + /** Companion description. */ + protected final String description; + + /** View on this companion, if any. */ + protected CompanionView view; + + /** Need for this companion. */ + protected Need need = Need.MANDATORY; + + /** Current installation status. */ + protected Status status = Status.NOT_INSTALLED; + + //~ Constructors ----------------------------------------------------------- + //-------------------// + // AbstractCompanion // + //-------------------// + /** + * Creates a new AbstractCompanion object. + * + * @param title the assigned title + */ + public AbstractCompanion (String title, + String description) + { + this.index = ++globalIndex; + this.title = title; + this.description = description; + } + + //~ Methods ---------------------------------------------------------------- + //----------------// + // checkInstalled // + //----------------// + @Override + public boolean checkInstalled () + { + status = getTargetFolder() + .exists() ? Status.INSTALLED : Status.NOT_INSTALLED; + + return status == Status.INSTALLED; + } + + //----------------// + // getDescription // + //----------------// + @Override + public String getDescription () + { + return description; + } + + //----------// + // getIndex // + //----------// + @Override + public int getIndex () + { + return index; + } + + //------------------// + // getInstallWeight // + //------------------// + @Override + public int getInstallWeight () + { + return isNeeded() ? 1 : 0; + } + + //---------// + // getNeed // + //---------// + @Override + public Need getNeed () + { + return need; + } + + //-----------// + // getStatus // + //-----------// + @Override + public Status getStatus () + { + return status; + } + + //----------// + // getTitle // + //----------// + @Override + public String getTitle () + { + return title; + } + + //--------------------// + // getUninstallWeight // + //--------------------// + @Override + public int getUninstallWeight () + { + return checkInstalled() ? 1 : 0; + } + + //---------// + // getView // + //---------// + @Override + public CompanionView getView () + { + return view; + } + + //---------// + // install // + //---------// + @Override + public void install () + throws Exception + { + try { + startInstallation(); + doInstall(); + completeInstallation(); + } catch (Exception ex) { + abortInstallation(ex); + throw ex; + } finally { + checkInstalled(); + updateView(); + } + } + + //----------// + // isNeeded // + //----------// + @Override + public boolean isNeeded () + { + return need != Need.NOT_SELECTED; + } + + //---------// + // setNeed // + //---------// + @Override + public void setNeed (Need need) + { + this.need = need; + } + + //-----------// + // uninstall // + //-----------// + @Override + public void uninstall () + { + try { + startUninstallation(); + doUninstall(); + completeUninstallation(); + } catch (Exception ex) { + abortUninstallation(ex); + } + } + + //---------------// + // appendCommand // + //---------------// + protected void appendCommand (String command) + { + Installer.getBundle() + .appendCommand(command); + } + + //-----------// + // doInstall // + //-----------// + /** + * Actual Installation. + */ + protected abstract void doInstall () + throws Exception; + + //-------------// + // doUninstall // + //-------------// + /** + * Actual Uninstallation. + */ + protected void doUninstall () + throws Exception + { + // Void by default + } + + //-----------------// + // getTargetFolder // + //-----------------// + /** + * Report the target folder, if any. + * This method must be overridden to provide precise target folder for a + * companion that uses a target folder. + * + * @return the target folder for this companion, or throw + * IllegalStateException if no folder is defined + */ + protected File getTargetFolder () + { + // By default + throw new IllegalStateException( + "No target folder is defined for " + getTitle()); + } + + //------------------// + // makeTargetFolder // + //------------------// + /** + * Create the target folder, if needed, and return it. + * + * @return the target folder for this companion, or throw + * IllegalStateException if no folder is defined + */ + protected File makeTargetFolder () + { + File folder = getTargetFolder(); + + if (!folder.exists()) { + if (folder.mkdirs()) { + logger.info("Created folder {}", folder.getAbsolutePath()); + } + } + + return folder; + } + + //------------// + // updateView // + //------------// + protected void updateView () + { + if (view != null) { + view.update(); + } + } + + //-------------------// + // abortInstallation // + //-------------------// + private void abortInstallation (Throwable ex) + { + status = Status.FAILED_TO_INSTALL; + + if (ex instanceof LicenseCompanion.LicenseDeclinedException) { + logger.warn(getTitle() + " declined."); + } else { + logger.warn(getTitle() + " failed to install.", ex); + } + } + + //---------------------// + // abortUninstallation // + //---------------------// + private void abortUninstallation (Throwable ex) + { + status = Status.FAILED_TO_UNINSTALL; + logger.warn(getTitle() + " failed to uninstall.", ex); + } + + //----------------------// + // completeInstallation // + //----------------------// + private void completeInstallation () + { + status = Status.INSTALLED; + logger.info("{} completed.", getTitle()); + } + + //------------------------// + // completeUninstallation // + //------------------------// + private void completeUninstallation () + { + status = Status.NOT_INSTALLED; + logger.info("{} completed.", getTitle()); + updateView(); + } + + //-------------------// + // startInstallation // + //-------------------// + private void startInstallation () + { + status = Status.BEING_INSTALLED; + logger.info("\n{} is being processed...", getTitle()); + updateView(); + } + + //---------------------// + // startUninstallation // + //---------------------// + private void startUninstallation () + { + status = Status.BEING_UNINSTALLED; + logger.info("\n{} is being processed...", getTitle()); + updateView(); + } +} diff --git a/src/installer/com/audiveris/installer/BasicCompanionView.java b/src/installer/com/audiveris/installer/BasicCompanionView.java new file mode 100644 index 0000000..c00a2b7 --- /dev/null +++ b/src/installer/com/audiveris/installer/BasicCompanionView.java @@ -0,0 +1,243 @@ +//----------------------------------------------------------------------------// +// // +// B a s i c C o m p a n i o n V i e w // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package com.audiveris.installer; + +import com.audiveris.installer.Companion.Need; +import com.audiveris.installer.Companion.Status; + +import com.jgoodies.forms.builder.PanelBuilder; +import com.jgoodies.forms.layout.CellConstraints; +import com.jgoodies.forms.layout.FormLayout; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.awt.Color; +import java.awt.Dimension; +import java.awt.Insets; +import java.awt.event.ItemEvent; +import java.awt.event.ItemListener; + +import javax.swing.JCheckBox; +import javax.swing.JComponent; +import javax.swing.JLabel; +import javax.swing.JPanel; + +/** + * Class {@code BasicCompanionView} is the basis for a View on a + * Companion. + * + * @author Hervé Bitteur + */ +public class BasicCompanionView + implements CompanionView, ItemListener +{ + //~ Static fields/initializers --------------------------------------------- + + /** Usual logger utility */ + private static final Logger logger = LoggerFactory.getLogger( + BasicCompanionView.class); + + /** Height of companion rectangle. */ + protected static final int HEIGHT = 40; + + //~ Instance fields -------------------------------------------------------- + + /** Related companion. */ + protected final Companion companion; + + /** Preferred width. */ + private final int width; + + /** Actual Swing component. */ + protected final JComponent component; + + /** Title. */ + protected final JLabel titleLabel = new JLabel(""); + + /** Need. */ + protected final JCheckBox needBox = new JCheckBox(""); + + //~ Constructors ----------------------------------------------------------- + + //--------------------// + // BasicCompanionView // + //--------------------// + /** + * Creates a new BasicCompanionView object. + * + * @param companion the related companion + */ + public BasicCompanionView (Companion companion, + int width) + { + this.companion = companion; + this.width = width; + + titleLabel.setText(companion.getIndex() + ") " + companion.getTitle()); + + component = defineLayout(); + component.setToolTipText(companion.getDescription()); + needBox.addItemListener(this); + } + + //~ Methods ---------------------------------------------------------------- + + //--------------// + // getCompanion // + //--------------// + @Override + public Companion getCompanion () + { + return companion; + } + + //--------------// + // getComponent // + //--------------// + @Override + public JComponent getComponent () + { + return component; + } + + //------------------// + // itemStateChanged // + //------------------// + @Override + public void itemStateChanged (ItemEvent evt) + { + // The need checkbox has changed + if (evt.getStateChange() == ItemEvent.SELECTED) { + companion.setNeed(Need.SELECTED); + } else { + companion.setNeed(Need.NOT_SELECTED); + } + + update(); + } + + //--------// + // update // + //--------// + @Override + public void update () + { + // Need + switch (companion.getNeed()) { + case MANDATORY : + needBox.setEnabled(false); + needBox.setSelected(true); + + break; + + case SELECTED : + needBox.setEnabled(true); + needBox.setSelected(true); + + break; + + case NOT_SELECTED : + needBox.setEnabled(true); + needBox.setSelected(false); + + break; + } + + // Status + component.setBackground( + getBackground(companion.getStatus(), companion.getNeed())); + component.repaint(); + } + + //---------------// + // getBackground // + //---------------// + protected Color getBackground (Status status, + Need need) + { + switch (companion.getStatus()) { + case NOT_INSTALLED : + + if (need != Need.NOT_SELECTED) { + return COLORS.NOT_INST; + } else { + return COLORS.UNUSED; + } + + case BEING_INSTALLED : + case BEING_UNINSTALLED : + return COLORS.BEING; + + case INSTALLED : + + if (need != Need.NOT_SELECTED) { + return COLORS.INST; + } else { + return COLORS.UNUSED; + } + + case FAILED_TO_INSTALL : + case FAILED_TO_UNINSTALL :default : + return COLORS.FAILED; + } + } + + //--------------// + // defineLayout // + //--------------// + private JPanel defineLayout () + { + // Prepare layout elements + final boolean optional = companion.getNeed() != Need.MANDATORY; + final CellConstraints cst = new CellConstraints(); + final String colSpec = optional ? "pref,1dlu,center:pref" + : "center:pref"; + final FormLayout layout = new FormLayout(colSpec, "center:20dlu"); + final JPanel panel = new MyPanel(); + final PanelBuilder builder = new PanelBuilder(layout, panel); + + // Now add the desired components, using provided order + if (optional) { + builder.add(needBox, cst.xy(1, 1)); + builder.add(titleLabel, cst.xy(3, 1)); + } else { + builder.add(titleLabel, cst.xy(1, 1)); + } + + panel.setPreferredSize(new Dimension(width, HEIGHT)); + panel.setOpaque(true); + + return panel; + } + + //~ Inner Classes ---------------------------------------------------------- + + //---------// + // MyPanel // + //---------// + private static class MyPanel + extends JPanel + { + //~ Static fields/initializers ----------------------------------------- + + private static final Insets insets = new Insets(3, 6, 3, 6); + + //~ Methods ------------------------------------------------------------ + + @Override + public Insets getInsets () + { + return insets; + } + } +} diff --git a/src/installer/com/audiveris/installer/Bundle.java b/src/installer/com/audiveris/installer/Bundle.java new file mode 100644 index 0000000..3d48e0e --- /dev/null +++ b/src/installer/com/audiveris/installer/Bundle.java @@ -0,0 +1,396 @@ +//----------------------------------------------------------------------------// +// // +// B u n d l e // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package com.audiveris.installer; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import javax.swing.JOptionPane; +import javax.swing.SwingUtilities; + +/** + * Class {@code Bundle} handles a sequence of Companions to install + * (or to uninstall). + * + * @author Hervé Bitteur + */ +public class Bundle +{ + //~ Static fields/initializers --------------------------------------------- + + /** Usual logger utility */ + private static final Logger logger = LoggerFactory.getLogger(Bundle.class); + + /** Environment descriptor. */ + private static final Descriptor descriptor = DescriptorFactory.getDescriptor(); + + //~ Instance fields -------------------------------------------------------- + /** Sequence of companions. */ + private final List companions = new ArrayList<>(); + + /** The install target folder, initialized to null. (not used) */ + private File installFolder; + + /** The specific companion in charge of OCR languages. */ + private OcrCompanion ocrCompanion; + + /** Related view on this bundle, if any. */ + private BundleView view; + + /** Flag for cancellation. */ + private boolean cancelled = false; + + /** Global list of commands to be run with admin privileges. */ + private final List commands = new ArrayList(); + + //~ Constructors ----------------------------------------------------------- + //--------// + // Bundle // + //--------// + /** + * Creates a new Bundle object. + */ + public Bundle (boolean hasUI) + { + createCompanions(); + + if (Installer.hasUI()) { + view = new BundleView(this); + + SwingUtilities.invokeLater(new Runnable() + { + @Override + public void run () + { + view.setVisible(true); + } + }); + } + } + + //~ Methods ---------------------------------------------------------------- + //---------------// + // appendCommand // + //---------------// + /** + * Append a command to the global list of admin commands. + * + * @param command the command to append + */ + public void appendCommand (String command) + { + if (!command.isEmpty()) { + logger.info("Posting: {}", command); + commands.add(command); + } + } + + //---------// + // getView // + //---------// + public BundleView getView () + { + return view; + } + + //-----------------// + // getOcrCompanion // + //-----------------// + public OcrCompanion getOcrCompanion () + { + return ocrCompanion; + } + + //--------------------// + // checkInstallations // + //--------------------// + public void checkInstallations () + { + for (Companion companion : companions) { + boolean installed = companion.checkInstalled(); + if (companion.isNeeded() && !installed) { + logger.debug("Installation needed for {}", companion.getTitle()); + } else { + logger.debug("{} needs no action", companion.getTitle()); + } + + if (companion.getView() != null) { + companion.getView() + .update(); + } + } + } + + //-------// + // close // + //-------// + public void close () + { + if (view != null) { + view.setVisible(false); + view.dispose(); + } + + Installer.latch.countDown(); + logger.info("Installer has stopped."); + } + + //---------------// + // getCompanions // + //---------------// + public List getCompanions () + { + return companions; + } + + //---------------// + // installBundle // + //---------------// + /** + * Install the bundle of needed companions. + * To avoid mixing system and user domains, the installation will proceed + * in two phases: first a user phase if needed (to install in proper user + * location using Java code) then a system phase if needed (to install + * system stuff using shell commands). + * + * @throws Exception + */ + public void installBundle () + throws Exception + { + logger.debug("installBundle"); + commands.clear(); + + // Compute total installation weight + int totalWeight = 0; + + for (Companion companion : companions) { + if (companion.isNeeded()) { + totalWeight += companion.getInstallWeight(); + } + } + + int progress = 0; + + if (totalWeight > 0) { + // First phase in user mode + for (Companion companion : companions) { + if (companion.isNeeded()) { + // Visual information + int weight = companion.getInstallWeight(); + Jnlp.extensionInstallerService.updateProgress( + (progress * 100) / totalWeight); + logger.debug("Processing {}", companion.getTitle()); + Jnlp.extensionInstallerService.setHeading( + companion.getIndex() + ") Processing " + companion.getTitle()); + if (view != null) { + Thread.sleep(100); // Let user see infos for a while + } + + // Install + if (!companion.checkInstalled()) { + companion.install(); + } else { + logger.debug("[{} is already installed]", companion.getTitle()); + } + progress += weight; + logger.debug("Progress: {}/{}", progress, totalWeight); + } + } + + // Second phase for commands in system mode + if (!commands.isEmpty()) { + Jnlp.extensionInstallerService.setHeading("System commands"); + logger.info("\nFinal commands to be run at admin level:"); + for (String command : commands) { + logger.info(" {}", command); + } + if (view != null) { + Thread.sleep(100); // Let user see infos for a while + } + JOptionPane.showMessageDialog( + Installer.getFrame(), + "To complete installation, you will now be prompted for" + + " administration privileges", + commands.size() + " Additional command(s) to be run", + JOptionPane.INFORMATION_MESSAGE); + + // One shell for all commands + try { + descriptor.runShell(!Installer.isAdmin, commands); + } catch (Exception ex) { + // Notify failure + Jnlp.extensionInstallerService.installFailed(); + throw ex; + } + } + + // Update status for all companions + checkInstallations(); + } + + Jnlp.extensionInstallerService.updateProgress(100); + } + + //-----------------// + // uninstallBundle // + //-----------------// + public void uninstallBundle () + throws Throwable + { + for (Companion companion : companions) { + if (companion.checkInstalled()) { + companion.uninstall(); + } + } + } + + //------------------// + // createCompanions // + //------------------// + private void createCompanions () + { + companions.add(new LicenseCompanion()); + companions.add(new CppCompanion()); + companions.add(new GhostscriptCompanion()); + companions.add(ocrCompanion = new OcrCompanion()); + companions.add(new DocCompanion()); + companions.add(new ExamplesCompanion()); + companions.add(new PluginsCompanion()); + companions.add(new TrainingCompanion()); + } + + //------------------// + // createTempFolder // + //------------------// + /** + * Create a fresh temp folder. + */ + public void createTempFolder () + throws IOException + { + File folder = descriptor.getTempFolder(); + if (!folder.exists()) { + // Create the folder + if (folder.mkdirs()) { + logger.info("Created folder {}", folder.getAbsolutePath()); + } + } + } + + //------------------// + // deleteTempFolder // + //------------------// + /** + * Remove the temp folder, with all its content. + */ + public void deleteTempFolder () + throws IOException + { + File folder = descriptor.getTempFolder(); + if (folder.exists()) { + // Clean up everything in the folder + while (true) { + try { + TreeRemover.remove(folder.toPath()); + break; + } catch (IOException ex) { + if (view != null) { + int opt = JOptionPane.showConfirmDialog( + Installer.getFrame(), + "Cannot delete installation temporary folder " + + "\nlocated at " + folder + + "\n" + + "\nMake sure no window or process is using it" + + ", then press OK", + "Installation temporary folder", + JOptionPane.OK_CANCEL_OPTION, + JOptionPane.WARNING_MESSAGE); + if (opt != JOptionPane.OK_OPTION) { + throw ex; + } + } else { + throw ex; + } + } + } + } + } + +// //------------------// +// // getInstallFolder // +// //------------------// +// /** +// * Report the install folder (as currently defined). +// * +// * @return the installFolder +// */ +// public File getInstallFolder () +// { +// if (installFolder == null) { +// // No user selection done, so let's use default folder +// installFolder = descriptor.getInstallFolder(); +// if (!installFolder.exists()) { +// if (installFolder.mkdirs()) { +// logger.info("Created folder {}", installFolder.getAbsolutePath()); +// } +// } +// } +// +// return installFolder; +// } +// +// //------------------// +// // setInstallFolder // +// //------------------// +// /** +// * Assign the install folder. +// * +// * @param installFolder the installFolder to set +// */ +// public void setInstallFolder (File installFolder) +// { +// this.installFolder = installFolder; +// +// // Update installation checks WRT this new folder +// checkInstallations(); +// } + //-------------// + // isCancelled // + //-------------// + /** + * Check for cancellation. + * + * @return the cancelled flag value + */ + public boolean isCancelled () + { + return cancelled; + } + + //--------------// + // setCancelled // + //--------------// + /** + * Set cancellation flag. + * + * @param cancelled the cancelled value to set + */ + public void setCancelled (boolean cancelled) + { + this.cancelled = cancelled; + } +} diff --git a/src/installer/com/audiveris/installer/BundleView.java b/src/installer/com/audiveris/installer/BundleView.java new file mode 100644 index 0000000..a1739f4 --- /dev/null +++ b/src/installer/com/audiveris/installer/BundleView.java @@ -0,0 +1,425 @@ +//----------------------------------------------------------------------------// +// // +// B u n d l e V i e w // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package com.audiveris.installer; + +import ch.qos.logback.classic.Level; + +import com.jgoodies.forms.builder.PanelBuilder; +import com.jgoodies.forms.layout.CellConstraints; +import com.jgoodies.forms.layout.FormLayout; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.awt.BorderLayout; +import java.awt.Color; +import java.awt.Container; +import java.awt.Dimension; +import java.awt.GraphicsConfiguration; +import java.awt.GraphicsDevice; +import java.awt.GraphicsEnvironment; +import java.awt.Insets; +import java.awt.Point; +import java.awt.Rectangle; +import java.awt.event.ActionEvent; + +import javax.swing.AbstractAction; +import javax.swing.Action; +import javax.swing.JButton; +import javax.swing.JFrame; +import javax.swing.JOptionPane; +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.SwingUtilities; +import javax.swing.SwingWorker; +import javax.swing.UIManager; +import javax.swing.UIManager.LookAndFeelInfo; + +/** + * Class {@code BundleView} is a View on a Bundle. + * + * @author Hervé Bitteur + */ +public class BundleView + extends JFrame +{ + //~ Static fields/initializers --------------------------------------------- + + private static final Logger logger = LoggerFactory.getLogger( + Installer.class); + + private static final Color INFO_BACKGROUND = new Color(250, 250, 255); + + private static final Color BUTTON_BACKGROUND = new Color(250, 250, 200); + + /** Cancel label for the stop action. */ + private static final String CANCEL = "Cancel"; + + /** Close label for the stop action. */ + private static final String CLOSE = "Close"; + + //~ Instance fields -------------------------------------------------------- + // + /** Related bundle. */ + private final Bundle bundle; + + /** Panel to display logged information. */ + private MessagePanel messagePanel; + + /** To start the installation. */ + private StartAction startAction; + + /** To stop (cancel or exit) the installation. */ + private StopAction stopAction; + + //~ Constructors ----------------------------------------------------------- + // + //------------// + // BundleView // + //------------// + /** + * Creates a new BundleView object. + * + * @param bundle the underlying bundle + */ + public BundleView (Bundle bundle) + { + super("Audiveris bundle installer"); + this.bundle = bundle; + + // Set panels opaque by default (TODO: useful?) + PanelBuilder.setOpaqueDefault(true); + + setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + + startAction = new StartAction(); + stopAction = new StopAction(); + + Container pane = getContentPane(); + pane.setLayout(new BorderLayout()); + + pane.add(buildNorthPanel(), BorderLayout.NORTH); + pane.add(buildInfoPanel(), BorderLayout.CENTER); + pane.add(buildButtonPanel(), BorderLayout.SOUTH); + + // Set Nimbus Look & Feel if possible + try { + for (LookAndFeelInfo info : UIManager.getInstalledLookAndFeels()) { + if ("Nimbus".equals(info.getName())) { + UIManager.setLookAndFeel(info.getClassName()); + SwingUtilities.updateComponentTreeUI(this); + + break; + } + } + } catch (Exception ex) { + logger.warn("Cannot set Nimbus L&F, using default.", ex); + } + + // Adjust size and location of this window + setSizeAndLocation(); + } + + //~ Methods ---------------------------------------------------------------- + //----------------// + // publishMessage // + //----------------// + public void publishMessage (Level level, + String message) + { + messagePanel.display(level, message); + } + + //------------------// + // buildButtonPanel // + //------------------// + /** + * Build the bottom panel that provides start and cancel buttons. + * + * @return the button panel + */ + private JPanel buildButtonPanel () + { + final JPanel panel = new JPanel(); + panel.setOpaque(true); + panel.setPreferredSize(new Dimension(500, 50)); + panel.setLayout(new BorderLayout()); + panel.setBackground(BUTTON_BACKGROUND); + + panel.add(new ButtonPanel(stopAction), BorderLayout.WEST); + panel.add(new ButtonPanel(startAction), BorderLayout.EAST); + + return panel; + } + + //----------------// + // buildCompPanel // + //----------------// + /** + * Build the sub-panel that displays the sequence of companions. + * + * @return the companion panel + */ + private JPanel buildCompPanel () + { + // Prepare layout elements + final String hGap = "$lcgap"; + final StringBuilder sbcol = new StringBuilder(); + + for (Companion companion : bundle.getCompanions()) { + sbcol.append("pref,") + .append(hGap) + .append(","); + } + + final CellConstraints cst = new CellConstraints(); + final FormLayout layout = new FormLayout(sbcol.toString(), "pref"); + final JPanel panel = new JPanel(); + final PanelBuilder builder = new PanelBuilder(layout, panel); + + // Now add the desired components, using provided order + int col = 1; + + for (Companion companion : bundle.getCompanions()) { + CompanionView view = companion.getView(); + builder.add(view.getComponent(), cst.xy(col, 1)); + col += 2; + } + + return panel; + } + + //----------------// + // buildInfoPanel // + //----------------// + private JScrollPane buildInfoPanel () + { + messagePanel = new MessagePanel(); + messagePanel.getComponent() + .setBackground(INFO_BACKGROUND); + + return messagePanel.getComponent(); + } + + //-----------------// + // buildNorthPanel // + //-----------------// + /** + * Build the top panel that displays the sequence of companions, + * the language selector and the install folder selector. + * + * @return the top panel + */ + private JPanel buildNorthPanel () + { + // Prepare layout elements + final FormLayout layout = new FormLayout( + "$lcgap, fill:0:grow, $lcgap", + "$rgap, pref, $rgap, pref, $rgap"); + final JPanel panel = new JPanel(); + final PanelBuilder builder = new PanelBuilder(layout, panel); + final CellConstraints cst = new CellConstraints(); + + int iRow = 0; + + // FolderSelector is currently disabled + // iRow +=2; + // // Add the folder selector + // FolderSelector dirSelector = new FolderSelector(bundle); + // builder.add(dirSelector.getComponent(), cst.xy(2, iRow)); + + // Add the languages component + iRow += 2; + + LangSelector langSelector = bundle.getOcrCompanion() + .getSelector(); + builder.add(langSelector.getComponent(), cst.xy(2, iRow)); + + // Add the companions component + iRow += 2; + builder.add(buildCompPanel(), cst.xy(2, iRow)); + + return panel; + } + + //--------------------// + // setSizeAndLocation // + //--------------------// + /** + * Try to locate this Bundle window so that the underlying JNLP + * window does not get masked by this one. + * We try to locate the window in the upper right corner of the primary + * physical screen. + */ + private void setSizeAndLocation () + { + pack(); + + // Window size + final int width = 687; + final int height = 400; + final int gapFromBorder = 20; + setSize(width, height); + + // Window location + GraphicsDevice device = GraphicsEnvironment.getLocalGraphicsEnvironment() + .getScreenDevices()[0]; + GraphicsConfiguration config = device.getConfigurations()[0]; + Rectangle bounds = config.getBounds(); + logger.debug("Primary screen bounds: {}", bounds); + + Point topLeft = new Point( + (bounds.x + bounds.width) - width - gapFromBorder, + bounds.y + gapFromBorder); + logger.debug("Window topLeft: {}", topLeft); + setLocation(topLeft); + } + + //~ Inner Classes ---------------------------------------------------------- + //-------------// + // ButtonPanel // + //-------------// + private static class ButtonPanel + extends JPanel + { + //~ Static fields/initializers ----------------------------------------- + + private static final Insets insets = new Insets(8, 5, 8, 5); + + //~ Constructors ------------------------------------------------------- + public ButtonPanel (Action action) + { + setPreferredSize(new Dimension(200, 0)); + + final JButton button = new JButton(action); + button.setPreferredSize(new Dimension(100, 25)); + setOpaque(false); + add(button); + } + + //~ Methods ------------------------------------------------------------ + @Override + public Insets getInsets () + { + return insets; + } + } + + //-------------// + // StartAction // + //-------------// + private class StartAction + extends AbstractAction + { + //~ Constructors ------------------------------------------------------- + + public StartAction () + { + super("Install"); + putValue(SHORT_DESCRIPTION, "Launch the installation"); + } + + //~ Methods ------------------------------------------------------------ + @Override + public void actionPerformed (ActionEvent e) + { + logger.debug("StartAction performed"); + + setEnabled(false); + stopAction.setEnabled(false); + new Worker().execute(); + } + } + + //------------// + // StopAction // + //------------// + private class StopAction + extends AbstractAction + { + //~ Constructors ------------------------------------------------------- + + public StopAction () + { + super(CANCEL); + putValue(SHORT_DESCRIPTION, "Cancel the installation"); + } + + //~ Methods ------------------------------------------------------------ + @Override + public void actionPerformed (ActionEvent e) + { + if (getValue(NAME) + .equals(CANCEL)) { + logger.debug("Cancel Action performed"); + bundle.setCancelled(true); + } else { + logger.debug("Close Action performed"); + } + + bundle.close(); + } + } + + //--------// + // Worker // + //--------// + private class Worker + extends SwingWorker + { + //~ Methods ------------------------------------------------------------ + + @Override + protected Void doInBackground () + throws Exception + { + try { + bundle.installBundle(); + logger.info("\nYou can now safely exit" + + ", Audiveris application will be launched.\n"); + if (Installer.hasUI()) { + JOptionPane.showMessageDialog( + Installer.getFrame(), + "Installation completed successfully", + "Installation completion", + JOptionPane.INFORMATION_MESSAGE); + } + + Jnlp.extensionInstallerService.installSucceeded(false); + } catch (Exception ex) { + if (Installer.hasUI()) { + JOptionPane.showMessageDialog( + Installer.getFrame(), + "Installation has failed: \n" + ex.getMessage(), + "Installation completion", + JOptionPane.WARNING_MESSAGE); + } + + Jnlp.extensionInstallerService.installFailed(); + } + + return null; + } + + @Override + protected void done () + { + startAction.setEnabled(true); + stopAction.setEnabled(true); + + stopAction.putValue(AbstractAction.NAME, CLOSE); + stopAction.putValue( + AbstractAction.SHORT_DESCRIPTION, + "Close the installer"); + } + } +} diff --git a/src/installer/com/audiveris/installer/Companion.java b/src/installer/com/audiveris/installer/Companion.java new file mode 100644 index 0000000..eebc0fa --- /dev/null +++ b/src/installer/com/audiveris/installer/Companion.java @@ -0,0 +1,120 @@ +//----------------------------------------------------------------------------// +// // +// C o m p a n i o n // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package com.audiveris.installer; + +/** + * Interface {@code Companion} defines a companion to install as part + * of Audiveris bundle. + * A companion does the maximum via Java code run in user mode. + * If some operation needs admin privilege then this operation (as small as + * possible) must be written as a command and appended to the bundle global + * command list which will be run at the end. + * + * @author Hervé Bitteur + */ +public interface Companion +{ + //~ Enumerations ----------------------------------------------------------- + + /** Need of Audiveris with respect to a companion. */ + enum Need + { + //~ Enumeration constant initializers ---------------------------------- + + /** + * Companion must be present. + */ + MANDATORY, + /** + * Optional companion but selected. + */ + SELECTED, + /** + * Optional companion not selected. + */ + NOT_SELECTED; + + } + + /** Current installation status of a companion. */ + enum Status + { + //~ Enumeration constant initializers ---------------------------------- + + /** + * Companion is not (yet) installed. + */ + NOT_INSTALLED, + /** + * Companion is being installed. + */ + BEING_INSTALLED, + /** + * Companion is being uninstalled. + */ + BEING_UNINSTALLED, + /** + * Companion is installed. + */ + INSTALLED, + /** + * Installation has failed. + */ + FAILED_TO_INSTALL, + /** + * Uninstallation has failed. + */ + FAILED_TO_UNINSTALL; + + } + + //~ Methods ---------------------------------------------------------------- + /** Check whether this companion has been installed. */ + boolean checkInstalled (); + + /** Report the full description. */ + String getDescription (); + + /** Report the companion index. */ + int getIndex (); + + /** Report a weight for installation. */ + int getInstallWeight (); + + /** Get companion need. */ + Need getNeed (); + + /** Get companion installation status. */ + Status getStatus (); + + /** Report the companion title for display. */ + String getTitle (); + + /** Report a weight for uninstallation. */ + int getUninstallWeight (); + + /** Report the related view, if any. */ + CompanionView getView (); + + /** Launch installation of this companion. */ + void install () + throws Exception; + + /** Check whether installation is needed. */ + boolean isNeeded (); + + /** Set companion need. */ + void setNeed (Need need); + + /** Launch de-installation of this companion. */ + void uninstall (); +} diff --git a/src/installer/com/audiveris/installer/CompanionView.java b/src/installer/com/audiveris/installer/CompanionView.java new file mode 100644 index 0000000..13b2cb2 --- /dev/null +++ b/src/installer/com/audiveris/installer/CompanionView.java @@ -0,0 +1,49 @@ +//----------------------------------------------------------------------------// +// // +// C o m p a n i o n V i e w // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package com.audiveris.installer; + +import java.awt.Color; + +import javax.swing.JComponent; + +/** + * Interface {@code CompanionView} defines a view on a {@link + * Companion} + * + * @author Hervé Bitteur + */ +public interface CompanionView +{ + //~ Methods ---------------------------------------------------------------- + + /** Report the underlying companion. */ + Companion getCompanion (); + + /** Report the Swing component. */ + JComponent getComponent (); + + /** Refresh the view from related companion. */ + void update (); + + //~ Inner Interfaces ------------------------------------------------------- + + interface COLORS + { + //~ Static fields/initializers ----------------------------------------- + + static final Color NOT_INST = new Color(245, 230, 220); + static final Color INST = new Color(200, 255, 200); + static final Color UNUSED = new Color(220, 220, 220); + static final Color BEING = new Color(255, 200, 0); + static final Color FAILED = new Color(255, 100, 100); + } +} diff --git a/src/installer/com/audiveris/installer/CppCompanion.java b/src/installer/com/audiveris/installer/CppCompanion.java new file mode 100644 index 0000000..fab7956 --- /dev/null +++ b/src/installer/com/audiveris/installer/CppCompanion.java @@ -0,0 +1,76 @@ +//----------------------------------------------------------------------------// +// // +// C p p C o m p a n i o n // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package com.audiveris.installer; + +import org.slf4j.LoggerFactory; + +/** + * Class {@code CppCompanion} handles installation of C++ runtime. + * + * @author Hervé Bitteur + */ +public class CppCompanion + extends AbstractCompanion +{ + //~ Static fields/initializers --------------------------------------------- + + /** Usual logger utility */ + private static final org.slf4j.Logger logger = LoggerFactory.getLogger( + CppCompanion.class); + + /** Environment descriptor. */ + private static final Descriptor descriptor = DescriptorFactory.getDescriptor(); + + private static final String DESC = "Audiveris Java application uses JNI" + + "
to access Tesseract C++ software." + + "
We thus need a specific version of" + + "
C++ runtime."; + + //~ Constructors ----------------------------------------------------------- + //--------------// + // CppCompanion // + //--------------// + /** + * Creates a new CppCompanion object. + */ + public CppCompanion () + { + super("C++", DESC); + + if (Installer.hasUI()) { + view = new BasicCompanionView(this, 50); + } + } + + //~ Methods ---------------------------------------------------------------- + //----------------// + // checkInstalled // + //----------------// + @Override + public boolean checkInstalled () + { + status = descriptor.isCppInstalled() ? Status.INSTALLED + : Status.NOT_INSTALLED; + + return status == Status.INSTALLED; + } + + //-----------// + // doInstall // + //-----------// + @Override + protected void doInstall () + throws Exception + { + descriptor.installCpp(); + } +} diff --git a/src/installer/com/audiveris/installer/Descriptor.java b/src/installer/com/audiveris/installer/Descriptor.java new file mode 100644 index 0000000..864fd5d --- /dev/null +++ b/src/installer/com/audiveris/installer/Descriptor.java @@ -0,0 +1,194 @@ +//----------------------------------------------------------------------------// +// // +// D e s c r i p t o r // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package com.audiveris.installer; + +import java.io.File; +import java.nio.file.Path; +import java.util.List; + +/** + * Interface {@code Descriptor} defines the features to be provided + * to the Installer by any target environment (os + arch). + * + *

This interface has been defined with generality in mind, but may have + * suffered from the bias of the initial Windows-based implementation. + * Hence, it still may evolve when we hit the development of implementations for + * Unix and for Mac. + * + * @author Hervé Bitteur + */ +public interface Descriptor +{ + //~ Static fields/initializers --------------------------------------------- + + /** ID for company developing audiveris software. */ + static final String COMPANY_ID = "AudiverisLtd"; + + /** Name for audiveris program. */ + static final String TOOL_NAME = "audiveris"; + + /** Name of environment variable for tessdata parent. */ + static final String TESSDATA_PREFIX = "TESSDATA_PREFIX"; + + /** Name of Tesseract folder. */ + static final String TESSERACT_OCR = "tesseract-ocr"; + + /** Name of tessdata folder. */ + static final String TESSDATA = "tessdata"; + + /** Minimum suitable version for Ghostscript. */ + static final String GHOSTSCRIPT_MIN_VERSION = "9.06"; + + //~ Methods ---------------------------------------------------------------- + /** + * Report the folder to be used for read-write configuration. + * + * @return the configuration folder + */ + File getConfigFolder (); + + /** + * Report the shell command to copy recursively source to target + * + * @param source path of source folder + * @param target path of target folder + * @return the proper shell command + */ + String getCopyCommand (Path source, + Path target); + + /** + * Report the folder to be used for read-write data. + * + * @return the data folder + */ + File getDataFolder (); + + /** + * Report the folder to be used for Tesseract-OCR if not yet set. + * If Tesseract application is installed, this folder is pointed by the + * environment variable TESSDATA_PREFIX, and this method is not called. + * If Tesseract application is not installed (and actually Audiveris does + * not need the application, but just the language files) then we call this + * method to know where we have to store the Tesseract trained data files. + * + * @return the parent folder of tessdata + */ + File getDefaultTessdataPrefix (); + + /** + * Report the shell command to delete a file + * + * @param file path of file to delete + * @return the proper shell command + */ + String getDeleteCommand (Path file); + + /** + * Report the shell command to create directories + * + * @param dir path to final folder + * @return the proper shell command + */ + String getMkdirCommand (Path dir); + + /** + * Report the shell command to set the executable flag on a file + * + * @param file path of file to set + * @return the proper shell command + */ + String getSetExecCommand (Path file); + + /** + * Report the collection of specific files to install + * + * @return the references of specific files for the target environment + */ + List getSpecificFiles (); + + /** + * Report a folder which can be used for temporary files created + * during installation. + * This folder is created anew when installation starts, and deleted when + * installation completes successfully. + * + * @return a temporary folder + */ + File getTempFolder (); + + /** + * Install the proper C++ runtime. + */ + void installCpp () + throws Exception; + + /** + * Install a suitable Ghostscript. + */ + void installGhostscript () + throws Exception; + + /** + * Install a suitable Tesseract. + */ + void installTesseract () + throws Exception; + + /** + * Report whether the current process is run with administrator + * privileges. + * + * @return true if admin + */ + boolean isAdmin (); + + /** + * Check whether the proper C++ runtime is installed. + * + * @return true if installed + */ + boolean isCppInstalled (); + + /** + * Check whether a suitable Ghostscript is installed. + * + * @return true if installed + */ + boolean isGhostscriptInstalled (); + + /** + * Check whether a suitable Tesseract is installed. + * + * @return true if installed + */ + boolean isTesseractInstalled (); + + /** + * Run a shell with admin privilege on the provided commands + * + * @param asAdmin true for elevated + * @param commands the list of commands to run + * @return true if OK + */ + boolean runShell (boolean asAdmin, + List commands) + throws Exception; + + /** + * Mark the provided file as executable. + * + * @param file the file to be set + */ + void setExecutable (Path file) + throws Exception; +} diff --git a/src/installer/com/audiveris/installer/DescriptorFactory.java b/src/installer/com/audiveris/installer/DescriptorFactory.java new file mode 100644 index 0000000..de1bd44 --- /dev/null +++ b/src/installer/com/audiveris/installer/DescriptorFactory.java @@ -0,0 +1,104 @@ +//----------------------------------------------------------------------------// +// // +// D e s c r i p t o r F a c t o r y // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package com.audiveris.installer; + +import com.audiveris.installer.unix.UnixDescriptor; +import com.audiveris.installer.windows.WindowsDescriptor; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Locale; + +/** + * Class {@code DescriptorFactory} is in charge of returning the + * suitable Descriptor instance for the current environment (os + arch). + * + * @author Hervé Bitteur + */ +public class DescriptorFactory +{ + //~ Static fields/initializers --------------------------------------------- + + /** Usual logger utility */ + private static final Logger logger = LoggerFactory.getLogger( + DescriptorFactory.class); + + /** Precise OS name. */ + private static final String OS_NAME = System.getProperty("os.name") + .toLowerCase(Locale.ENGLISH); + + /** Precise OS architecture. */ + public static final String OS_ARCH = System.getProperty("os.arch") + .toLowerCase(Locale.ENGLISH); + + /** Are we using a Linux OS?. */ + public static final boolean LINUX = OS_NAME.startsWith("linux"); + + /** Are we using a Mac OS?. */ + public static final boolean MAC_OS_X = OS_NAME.startsWith("mac os x"); + + /** Are we using a Windows OS?. */ + public static final boolean WINDOWS = OS_NAME.startsWith("windows"); + + /** Are we using Windows on 64 bit architecture?. */ + public static final boolean WINDOWS_64 = WINDOWS + && (System.getenv( + "ProgramFiles(x86)") != null); + + /** Are we running wow (appli-32/windows-64) or pure (32/32 - 64/64)?. */ + public static final boolean WOW = OS_ARCH.equals("x86") && WINDOWS_64; + + /** THE descriptor instance. */ + private static Descriptor descriptor; + + //~ Constructors ----------------------------------------------------------- + /** Not meant to be instantiated. */ + private DescriptorFactory () + { + } + + //~ Methods ---------------------------------------------------------------- + //----------------// + // geteDescriptor // + //----------------// + public static Descriptor getDescriptor () + { + if (descriptor == null) { + descriptor = createDescriptor(); + } + + return descriptor; + } + + //------------------// + // createDescriptor // + //------------------// + private static Descriptor createDescriptor () + { + if (WINDOWS) { + logger.debug("Creating a Windows descriptor"); + + return new WindowsDescriptor(); + } + + if (LINUX) { + logger.debug("Creating a Unix descriptor"); + + return new UnixDescriptor(); + } + + // For all others... + throw new UnsupportedEnvironmentException( + "name: " + OS_NAME + ", arch: " + OS_ARCH); + } +} diff --git a/src/installer/com/audiveris/installer/DocCompanion.java b/src/installer/com/audiveris/installer/DocCompanion.java new file mode 100644 index 0000000..8ae9fa5 --- /dev/null +++ b/src/installer/com/audiveris/installer/DocCompanion.java @@ -0,0 +1,278 @@ +//----------------------------------------------------------------------------// +// // +// D o c C o m p a n i o n // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package com.audiveris.installer; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.net.URI; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardCopyOption; +import java.util.jar.JarFile; +import java.util.zip.ZipEntry; +import javax.swing.JOptionPane; + +/** + * Class {@code DocCompanion} handles the local installation of + * documentation. + *

Since it's the only mandatory general purpose companion, it also handles: + *

    + *
  • the creation of the data directories + * ("benches", "eval", "print", "scores", "scripts").
  • + *
  • the creation of specific files (.bat and .sh)
  • + *
+ * + * @author Hervé Bitteur + */ +public class DocCompanion + extends AbstractCompanion +{ + //~ Static fields/initializers --------------------------------------------- + + /** Usual logger utility */ + private static final Logger logger = LoggerFactory.getLogger( + DocCompanion.class); + + /** Environment descriptor. */ + private static final Descriptor descriptor = DescriptorFactory.getDescriptor(); + + /** Tooltip. */ + private static final String DESC = + "The whole Audiveris documentation is made" + + "
available in one local browsable folder."; + + /** List of sub-folders created in Data folder. */ + private static final String[] dataFolders = new String[]{ + "benches", "eval", "print", "scores", "scripts"}; + + //~ Constructors ----------------------------------------------------------- + //--------------// + // DocCompanion // + //--------------// + /** + * Creates a new DocCompanion object. + */ + public DocCompanion () + { + super("Docs", DESC); + + if (Installer.hasUI()) { + view = new BasicCompanionView(this, 60); + } + } + + //~ Methods ---------------------------------------------------------------- + //----------------// + // checkInstalled // + //----------------// + @Override + public boolean checkInstalled () + { + if (getTargetFolder().exists() + && descriptor.getConfigFolder().exists() + && checkDirectories(descriptor.getDataFolder(), dataFolders) + && checkSpecificFiles()) { + status = Status.INSTALLED; + } else { + status = Status.NOT_INSTALLED; + } + + return status == Status.INSTALLED; + } + + //-----------// + // doInstall // + //-----------// + @Override + protected void doInstall () + throws Exception + { + final URI codeBase = Jnlp.basicService.getCodeBase() + .toURI(); + final URL url = Utilities.toURI( + codeBase, + "resources/documentation.jar") + .toURL(); + Utilities.downloadJarAndExpand( + "Docs", + url.toString(), + descriptor.getTempFolder(), + "www", + makeTargetFolder()); + + // Also, create user config directories + //TODO: should populate logging-config.xml, run.properties, user-actions.xml + createDirectories(descriptor.getConfigFolder()); + + // Create empty user data directories + createDirectories(descriptor.getDataFolder(), dataFolders); + + // Install some specific files + installSpecificFiles(); + } + + //-----------------// + // getTargetFolder // + //-----------------// + @Override + protected File getTargetFolder () + { + return new File(descriptor.getDataFolder(), "www"); + } + + //-------------------// + // createDirectories // + //-------------------// + private void createDirectories (File root, + String... names) + { + if (root.mkdirs()) { + logger.info("Created folder {}", root.getAbsolutePath()); + } + + for (String name : names) { + File folder = new File(root, name); + + if (folder.mkdirs()) { + logger.info("Created folder {}", folder.getAbsolutePath()); + } + } + } + + //------------------// + // checkDirectories // + //------------------// + private boolean checkDirectories (File root, + String... names) + { + if (!root.exists()) { + return false; + } + + for (String name : names) { + File folder = new File(root, name); + + if (!folder.exists()) { + return false; + } + } + + return true; + } + + //----------------------// + // installSpecificFiles // + //----------------------// + /** + * Install specific files for the current OS. + * For the time being, we assume that all these files are located in + * writable locations. If this assumption turns out to be false, we'll have + * to fall back to posting shell commands (Not yet implemented). + * + * @throws Exception + */ + private void installSpecificFiles () + throws Exception + { + // Download the global archive of specific files + final URI codeBase = Jnlp.basicService.getCodeBase().toURI(); + final URL url = Utilities.toURI(codeBase, "resources/specifics.jar") + .toURL(); + final String jarName = new File(url.toString()).getName(); + final File jarFile = new File(descriptor.getTempFolder(), jarName); + Utilities.download(url.toString(), jarFile); + + final JarFile jar = new JarFile(jarFile); + + // Process each desired specific file + for (SpecificFile specificFile : descriptor.getSpecificFiles()) { + // Make sure the target folder exists + final Path target = Paths.get(specificFile.target); + final Path parent = target.getParent(); + + if (!Files.exists(parent)) { + try { + Files.createDirectories(parent); + logger.info("Created dir {}", parent); + } catch (IOException ex) { + logger.debug("Could not directly create dirs {}" + + ", will post command at system level", parent); + // Resort to shell + appendCommand(descriptor.getMkdirCommand(parent)); + } + } + + // Copy the source entry to the target file + ZipEntry entry = jar.getEntry(specificFile.source); + if (entry != null) { + try (InputStream is = jar.getInputStream(entry)) { + // May fail + Files.copy(is, target, StandardCopyOption.REPLACE_EXISTING); + logger.info("Specific {} copied", target); + if (specificFile.isExec) { + try { + // May fail (but copy must have failed before) + descriptor.setExecutable(target); + } catch (Exception ignored) { + // User already warned, proceed to next file + } + } + } catch (IOException ex) { + logger.debug("Could not directly write {}" + + ", will post command at system level", target); + // Resort to local copy, then shell command + Path local = Paths.get(descriptor.getTempFolder().toString(), + target.getFileName().toString()); + try (InputStream is = jar.getInputStream(entry)) { + Files.copy(is, local, StandardCopyOption.REPLACE_EXISTING); + logger.info("Specific {} copied locally", target); + // Here local and target are regular files (not folders) + appendCommand(descriptor.getCopyCommand(local, parent)); + if (specificFile.isExec) { + appendCommand(descriptor.getSetExecCommand(target)); + } + } + } + } else { + // We can live without these files, so warn the user + // but keep the installation going. + String msg = "No entry " + specificFile.source + "\n in " + url; + logger.warn(msg); + JOptionPane.showMessageDialog( + Installer.getFrame(), + msg, + "Entry not found", + JOptionPane.WARNING_MESSAGE); + } + } + } + + //--------------------// + // checkSpecificFiles // + //--------------------// + private boolean checkSpecificFiles () + { + for (SpecificFile specificFile : descriptor.getSpecificFiles()) { + if (!new File(specificFile.target).exists()) { + return false; + } + } + + return true; + } +} diff --git a/src/installer/com/audiveris/installer/ExamplesCompanion.java b/src/installer/com/audiveris/installer/ExamplesCompanion.java new file mode 100644 index 0000000..c314433 --- /dev/null +++ b/src/installer/com/audiveris/installer/ExamplesCompanion.java @@ -0,0 +1,85 @@ +//----------------------------------------------------------------------------// +// // +// E x a m p l e s C o m p a n i o n // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package com.audiveris.installer; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.net.URI; +import java.net.URL; + +/** + * Class {@code ExamplesCompanion} handles the installation of a few + * image examples for Audiveris. + * + * @author Hervé Bitteur + */ +public class ExamplesCompanion + extends AbstractCompanion +{ + //~ Static fields/initializers --------------------------------------------- + + /** Usual logger utility */ + private static final Logger logger = LoggerFactory.getLogger( + ExamplesCompanion.class); + + private static final String DESC = "[This component is optional]" + + "
It installs a few image examples."; + + /** Environment descriptor. */ + private static final Descriptor descriptor = DescriptorFactory.getDescriptor(); + + //~ Constructors ----------------------------------------------------------- + /** + * Creates a new ExamplesCompanion object. + */ + public ExamplesCompanion () + { + super("Examples", DESC); + need = Need.SELECTED; + + if (Installer.hasUI()) { + view = new BasicCompanionView(this, 105); + } + } + + //~ Methods ---------------------------------------------------------------- + //-----------// + // doInstall // + //-----------// + @Override + protected void doInstall () + throws Exception + { + final URI codeBase = Jnlp.basicService.getCodeBase() + .toURI(); + final URL url = Utilities.toURI(codeBase, "resources/examples.jar") + .toURL(); + + Utilities.downloadJarAndExpand( + "Examples", + url.toString(), + descriptor.getTempFolder(), + "examples", + makeTargetFolder()); + } + + //-----------------// + // getTargetFolder // + //-----------------// + @Override + protected File getTargetFolder () + { + return new File(descriptor.getDataFolder(), "examples"); + } +} diff --git a/src/installer/com/audiveris/installer/Expander.java b/src/installer/com/audiveris/installer/Expander.java new file mode 100644 index 0000000..3e5c4f3 --- /dev/null +++ b/src/installer/com/audiveris/installer/Expander.java @@ -0,0 +1,179 @@ +//----------------------------------------------------------------------------// +// // +// E x p a n d e r // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package com.audiveris.installer; + +import org.apache.commons.compress.archivers.ArchiveEntry; +import org.apache.commons.compress.archivers.ArchiveException; +import org.apache.commons.compress.archivers.ArchiveStreamFactory; +import org.apache.commons.compress.archivers.tar.TarArchiveInputStream; +import org.apache.commons.compress.utils.IOUtils; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.List; +import java.util.zip.GZIPInputStream; + +/** + * Class {@code Expander} gathers methods to expand a .gz or a .tar + * file. + * These methods can be used to expand a .tar.gz archive. + * + * @author Dan Borza (at http://stackoverflow.com/users/510638/dan-borza) + * @author Hervé Bitteur + */ +public final class Expander +{ + //~ Static fields/initializers --------------------------------------------- + + /** Usual logger utility */ + private static final Logger logger = LoggerFactory.getLogger( + Expander.class); + + /** .gz extension. */ + private static final String GZ_EXT = ".gz"; + + /** .tar extension. */ + private static final String TAR_EXT = ".tar"; + + //~ Constructors ----------------------------------------------------------- + /** Not meant to be instantiated */ + private Expander () + { + } + + //~ Methods ---------------------------------------------------------------- + //--------------// + // streamToFile // + //--------------// + /** + * Store the provided input stream to the desired file. + * + * @param in the input stream. It is not closed by this method. + * @param outFile the desired target file + * @return the target file + * @throws FileNotFoundException + * @throws IOException + */ + public static File streamToFile (final InputStream in, + final File outFile) + throws FileNotFoundException, IOException + { + final OutputStream out = new FileOutputStream(outFile); + IOUtils.copy(in, out); + out.close(); + + return outFile; + } + + //--------// + // unGzip // + //--------// + /** + * Ungzip an input file into an output file. + * + * The output file is created in the output folder, having the same name + * as the input file, minus the '.gz' extension. + * + * @param inFile the input .gz file + * @param outDir the output directory file. + * @throws IOException + * @throws FileNotFoundException + * + * @return The file with the ungzipped content. + */ + public static File unGzip (final File inFile, + final File outDir) + throws FileNotFoundException, IOException + { + logger.debug("Ungzipping {} to dir {}", inFile, outDir); + + final String inName = inFile.getName(); + assert inName.endsWith(GZ_EXT); + + final InputStream in = new GZIPInputStream(new FileInputStream(inFile)); + final File outFile = streamToFile( + in, + new File( + outDir, + inName.substring(0, inName.length() - GZ_EXT.length()))); + in.close(); + + return outFile; + } + + //-------// + // unTar // + //-------// + /** + * Untar an input file into an output directory. + * + * @param inFile the input .tar file + * @param outDir the output directory + * @throws IOException + * @throws FileNotFoundException + * @throws ArchiveException + * + * @return The list of files with the untared content. + */ + public static List unTar (final File inFile, + final File outDir) + throws FileNotFoundException, IOException, ArchiveException + { + logger.debug("Untaring {} to dir {}", inFile, outDir); + assert inFile.getName() + .endsWith(TAR_EXT); + + final List untaredFiles = new ArrayList(); + final InputStream is = new FileInputStream(inFile); + final TarArchiveInputStream tis = (TarArchiveInputStream) new ArchiveStreamFactory().createArchiveInputStream( + "tar", + is); + ArchiveEntry entry; + + while ((entry = tis.getNextEntry()) != null) { + final File outFile = new File(outDir, entry.getName()); + + if (entry.isDirectory()) { + logger.debug("Attempting to write output dir {}", outFile); + + if (!outFile.exists()) { + logger.debug("Attempting to create output dir {}", outFile); + + if (!outFile.mkdirs()) { + throw new IllegalStateException( + String.format( + "Couldn't create directory %s", + outFile.getAbsolutePath())); + } + } + } else { + logger.debug("Creating output file {}", outFile); + streamToFile(tis, outFile); + } + + untaredFiles.add(outFile); + } + + tis.close(); + + return untaredFiles; + } +} diff --git a/src/installer/com/audiveris/installer/FileCopier.java b/src/installer/com/audiveris/installer/FileCopier.java new file mode 100644 index 0000000..fc88794 --- /dev/null +++ b/src/installer/com/audiveris/installer/FileCopier.java @@ -0,0 +1,179 @@ +//----------------------------------------------------------------------------// +// // +// F i l e C o p i e r // +// // +//----------------------------------------------------------------------------// +// +// Copyright © Herve 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. +//----------------------------------------------------------------------------// +// +package com.audiveris.installer; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.nio.file.FileVisitResult; +import static java.nio.file.FileVisitResult.*; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.SimpleFileVisitor; +import static java.nio.file.StandardCopyOption.*; +import java.nio.file.attribute.BasicFileAttributes; +import java.util.Objects; + +/** + * Class {@code FileCopier} copies a collection of source files + * to a target. + *

+ * If several source files are provided, the target must be a directory. + * If a source file is a directory and if the {@code recursive} flag is set, + * then the directory content is copied recursively. + * + * @author Hervé Bitteur + */ +public class FileCopier +{ + //~ Static fields/initializers --------------------------------------------- + + private static final Logger logger = LoggerFactory.getLogger( + FileCopier.class); + + //~ Instance fields -------------------------------------------------------- + + /** Sources. */ + private final Path[] sources; + + /** Target. */ + private final Path target; + + /** Recursive flag. */ + private final boolean recursive; + + //~ Constructors ----------------------------------------------------------- + + //------------// + // FileCopier // + //------------// + /** + * Creates a new FileCopier object. + * + * @param sources the sources collection, perhaps empty but not null + * @param target the target file (perhaps a folder) + * @param recursive true for a recursive copy + */ + public FileCopier (Path[] sources, + Path target, + boolean recursive) + { + Objects.requireNonNull(sources, "Sources cannot be null"); + + // Make a deep copy of sources collection + this.sources = new Path[sources.length]; + + for (int i = 0; i < sources.length; i++) { + this.sources[i] = sources[i]; + } + + Objects.requireNonNull(target, "Target cannot be null"); + this.target = target; + + this.recursive = recursive; + } + + //~ Methods ---------------------------------------------------------------- + + //------// + // copy // + //------// + /** + * Perform the copy. + */ + public void copy () + throws IOException + { + // Is target a directory? + boolean isDir = Files.isDirectory(target); + + // Copy each source (file or folder) to target + for (Path source : sources) { + Path dest = isDir ? target.resolve(source.getFileName()) : target; + + try { + if (recursive) { + Files.walkFileTree(source, new TreeCopier(source, dest)); + } else { + if (Files.isDirectory(source)) { + logger.warn("{} is a directory", source); + } else { + Files.copy(source, dest, REPLACE_EXISTING); + } + } + } catch (IOException ex) { + logger.info( + "Cannot copy '" + source + "' to '" + dest + "' ex:" + ex); + throw ex; + } + } + } + + //~ Inner Classes ---------------------------------------------------------- + + //------------// + // TreeCopier // + //------------// + /** + * TreeCopier is meant to recursively copy a tree of folders + * rooted at {@code source} to {@code dest} folder. + */ + private class TreeCopier + extends SimpleFileVisitor + { + //~ Instance fields ---------------------------------------------------- + + private final Path source; + private final Path dest; + + //~ Constructors ------------------------------------------------------- + + public TreeCopier (Path source, + Path dest) + { + this.source = source; + this.dest = dest; + } + + //~ Methods ------------------------------------------------------------ + + @Override + public FileVisitResult preVisitDirectory (Path dir, + BasicFileAttributes attrs) + throws IOException + { + Path newFolder = dest.resolve(source.relativize(dir)); + + if (Files.notExists(newFolder)) { + logger.debug("Creating folder {}", newFolder); + Files.copy(dir, newFolder, REPLACE_EXISTING); + } else { + logger.debug("Folder {} already exists.", newFolder); + } + + return CONTINUE; + } + + @Override + public FileVisitResult visitFile (Path file, + BasicFileAttributes attrs) + throws IOException + { + final Path destPath = dest.resolve(source.relativize(file)); + logger.debug("Copying file {} to {}", file, destPath); + Files.copy(file, destPath, REPLACE_EXISTING); + + return CONTINUE; + } + } +} diff --git a/src/installer/com/audiveris/installer/FolderSelector.java b/src/installer/com/audiveris/installer/FolderSelector.java new file mode 100644 index 0000000..592b151 --- /dev/null +++ b/src/installer/com/audiveris/installer/FolderSelector.java @@ -0,0 +1,206 @@ +//----------------------------------------------------------------------------// +// // +// F o l d e r S e l e c t o r // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package com.audiveris.installer; + +import com.jgoodies.forms.builder.PanelBuilder; +import com.jgoodies.forms.layout.CellConstraints; +import com.jgoodies.forms.layout.FormLayout; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.io.File; + +import javax.swing.AbstractAction; +import javax.swing.JButton; +import javax.swing.JComponent; +import javax.swing.JFileChooser; +import javax.swing.JOptionPane; +import javax.swing.JPanel; +import javax.swing.JTextField; + +/** + * Class {@code FolderSelector} displays the default install folder, + * and let the user choose another one, if any. + * + * NOTA: THIS CLASS IS NOT USED FOR THE TIME BEING + * Since the notion of installation folder is not clear! + * - With Java Web Start, appli "program data" is located in Java cache + * - On Linux, appli "user config" and appli "user data" do not have the same + * parent + * + * @author Hervé Bitteur + */ +@Deprecated +public class FolderSelector + implements ActionListener +{ + //~ Static fields/initializers --------------------------------------------- + + /** Usual logger utility */ + private static final Logger logger = LoggerFactory.getLogger( + FolderSelector.class); + + /** Environment descriptor. */ + private static final Descriptor descriptor = DescriptorFactory.getDescriptor(); + + //~ Instance fields -------------------------------------------------------- + /** The containing bundle. */ + private final Bundle bundle; + + /** The swing component. */ + private JComponent component; + + /** Text field for folder path. */ + private JTextField path = new JTextField(); + + //~ Constructors ----------------------------------------------------------- + //----------------// + // FolderSelector // + //----------------// + /** + * Creates a new FolderSelector object. + */ + public FolderSelector (Bundle bundle) + { + this.bundle = bundle; + + component = defineLayout(); + + ///////path.setText(descriptor.getInstallFolder().getAbsolutePath()); + path.addActionListener(this); + } + + //~ Methods ---------------------------------------------------------------- + //-----------------// + // actionPerformed // + //-----------------// + /** + * Triggered from path JTextField. + * + * @param e the action event + */ + @Override + public void actionPerformed (ActionEvent e) + { + logger.debug("Got event {}", e); + checkFolder(new File(path.getText())); + } + + //--------------// + // getComponent // + //--------------// + /** + * @return the component + */ + public JComponent getComponent () + { + return component; + } + + //-------------// + // checkFolder // + //-------------// + /** + * Make sure the provided candidate is OK + * + * @param file the candidate folder + */ + private void checkFolder (File candidate) + { + try { + if (!candidate.exists()) { + // Make sure all directories are created + if (candidate.mkdirs()) { + logger.info("Created folder {}", candidate.getAbsolutePath()); + } + } else { + if (!candidate.isDirectory()) { + throw new IllegalStateException("Not a directory"); + } + } + + // All tests are OK + path.setText(candidate.getAbsolutePath()); + ////////bundle.setInstallFolder(candidate); + } catch (Exception ex) { + if (Installer.hasUI()) { + JOptionPane.showMessageDialog( + Installer.getFrame(), + "Invalid folder: " + ex, + "Folder selection", + JOptionPane.WARNING_MESSAGE); + } + } + } + + //--------------// + // defineLayout // + private JComponent defineLayout () + { + final JPanel comp = new JPanel(); + final FormLayout layout = new FormLayout( + "right:36dlu, $lcgap, fill:0:grow, $lcgap, 31dlu", + "pref"); + final CellConstraints cst = new CellConstraints(); + final PanelBuilder builder = new PanelBuilder(layout, comp); + + // Label on left side + builder.addROLabel("Folder", cst.xy(1, 1)); + + // Path to folder + builder.add(path, cst.xy(3, 1)); + + // "Select" button on left side + builder.add(new JButton(new BrowseAction()), cst.xy(5, 1)); + + return comp; + } + + //~ Inner Classes ---------------------------------------------------------- + //--------------// + // BrowseAction // + //--------------// + private class BrowseAction + extends AbstractAction + { + //~ Constructors ------------------------------------------------------- + + public BrowseAction () + { + putValue(AbstractAction.NAME, "Select"); + putValue( + AbstractAction.SHORT_DESCRIPTION, + "Select another installation folder"); + } + + //~ Methods ------------------------------------------------------------ + @Override + public void actionPerformed (ActionEvent e) + { +// // We always launch browsing from default installation folder +// JFileChooser chooser = new JFileChooser( +// descriptor.getInstallFolder()); +// chooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY); +// +// int opt = chooser.showDialog( +// Installer.getFrame(), +// "Select install folder"); +// +// if (opt == JFileChooser.APPROVE_OPTION) { +// checkFolder(chooser.getSelectedFile()); +// } + } + } +} diff --git a/src/installer/com/audiveris/installer/GhostscriptCompanion.java b/src/installer/com/audiveris/installer/GhostscriptCompanion.java new file mode 100644 index 0000000..ccc9100 --- /dev/null +++ b/src/installer/com/audiveris/installer/GhostscriptCompanion.java @@ -0,0 +1,75 @@ +//----------------------------------------------------------------------------// +// // +// G h o s t s c r i p t C o m p a n i o n // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package com.audiveris.installer; + +import org.slf4j.LoggerFactory; + +/** + * Class {@code GhostscriptCompanion} handles the installation of + * Ghostscript. + * + * @author Hervé Bitteur + */ +class GhostscriptCompanion + extends AbstractCompanion +{ + //~ Static fields/initializers --------------------------------------------- + + /** Usual logger utility */ + private static final org.slf4j.Logger logger = LoggerFactory.getLogger( + GhostscriptCompanion.class); + + private static final String DESC = "Ghostscript component is mandatory." + + "
It allows to process PDF input files."; + + /** Environment descriptor. */ + private static final Descriptor descriptor = DescriptorFactory.getDescriptor(); + + //~ Constructors ----------------------------------------------------------- + //----------------------// + // GhostscriptCompanion // + //----------------------// + /** + * Creates a new GhostscriptCompanion object. + */ + public GhostscriptCompanion () + { + super("Ghostscript", DESC); + + if (Installer.hasUI()) { + view = new BasicCompanionView(this, 95); + } + } + + //~ Methods ---------------------------------------------------------------- + //----------------// + // checkInstalled // + //----------------// + @Override + public boolean checkInstalled () + { + status = descriptor.isGhostscriptInstalled() ? Status.INSTALLED + : Status.NOT_INSTALLED; + + return status == Status.INSTALLED; + } + + //-----------// + // doInstall // + //-----------// + @Override + protected void doInstall () + throws Exception + { + descriptor.installGhostscript(); + } +} diff --git a/src/installer/com/audiveris/installer/Installer.java b/src/installer/com/audiveris/installer/Installer.java new file mode 100644 index 0000000..333c9ef --- /dev/null +++ b/src/installer/com/audiveris/installer/Installer.java @@ -0,0 +1,313 @@ +//----------------------------------------------------------------------------// +// // +// I n s t a l l e r // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package com.audiveris.installer; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Arrays; +import java.util.concurrent.CountDownLatch; + +import javax.jnlp.UnavailableServiceException; +import javax.swing.JOptionPane; + +/** + * Class {@code Installer} is the main class for installation of + * Audiveris complete bundle using Java Web Start technology. + *

+ * We can assume that, thanks to Java Web Start, a proper JRE version is made + * available for this installer before it is launched. + * Since subsequent Audiveris application needs java 1.7 as of this writing, + * we can base this Installer on Java 1.7 as well. + * + * @author Hervé Bitteur + */ +public class Installer +{ + //~ Static fields/initializers --------------------------------------------- + + static { + /** Configure logging. */ + LogUtilities.initialize(); + } + + /** Usual logger utility */ + private static final Logger logger = LoggerFactory.getLogger( + Installer.class); + + /** Single instance. */ + private static Installer INSTANCE; + + /** Flag an installation. */ + private static boolean isInstall; + + /** Flag an uninstallation. */ + private static boolean isUninstall; + + /** Flag a process running as administrator. */ + public static boolean isAdmin; + + /** Flag an interactive (vs batch) run. */ + private static boolean hasUI; + + /** To force main thread to wait for UI completion. */ + public static CountDownLatch latch = new CountDownLatch(1); + + //~ Instance fields -------------------------------------------------------- + // + /** Bundle of companions to install (or insinstall). */ + private final Bundle bundle; + + //~ Constructors ----------------------------------------------------------- + //-----------// + // Installer // + //-----------// + /** + * Creates a new Installer object. + */ + private Installer () + { + Jnlp.extensionInstallerService.setHeading( + "Running Audiveris installer."); + + // Handling the bundle of all companions + bundle = new Bundle(hasUI); + } + + //~ Methods ---------------------------------------------------------------- + //-----------// + // getBundle // + //-----------// + /** + * Report the bundle of companions to process. + * + * @return the bundle of companions. + */ + public static Bundle getBundle () + { + if (INSTANCE == null) { + return null; + } + + return INSTANCE.bundle; + } + + //----------// + // getFrame // + //----------// + /** + * Convenient method to get access to the Installer frame. + * Useful for example to locate dialogs with respect to this frame. + * + * @return the Installer frame + */ + public static BundleView getFrame () + { + Bundle bundle = getBundle(); + + return (bundle != null) ? bundle.getView() : null; + } + + //-------// + // hasUI // + //-------// + /** + * Report whether the Installer is run in interactive mode. + * + * @return true if interactive + */ + public static boolean hasUI () + { + return hasUI; + } + + //---------// + // install // + //---------// + /** + * Launch installation. + */ + public void install () + { + logger.debug("install"); + + try { + // Use a fresh install folder + bundle.deleteTempFolder(); + bundle.createTempFolder(); + + // Get all initial installation statuses + bundle.checkInstallations(); + + ///installerService.hideProgressBar(); + if (hasUI) { + logger.info( + "\nEnvironment:\n" + + "- OS: {}\n" + + "- Architecture: {}\n" + + "- Java VM: {}\n", + System.getProperty("os.name") + " " + + System.getProperty("os.version"), + System.getProperty("os.arch"), + System.getProperty("java.vm.name") + " (build " + + System.getProperty("java.vm.version") + ", " + + System.getProperty("java.vm.info") + ")"); + logger.info( + "Please:\n" + + "- Add or remove OCR-supported languages,\n" + + "- Check or uncheck optional components,\n" + + "- Launch installation.\n"); + + // Wait until UI has finished... + latch.await(); + + // To avoid immediate closing... + // JOptionPane.showMessageDialog( + // Installer.getFrame(), + // "Closing time!", + // "This is the end", + // JOptionPane.INFORMATION_MESSAGE); + + // What if the user has simply not launched the install? + if (bundle.isCancelled()) { + Jnlp.extensionInstallerService.installFailed(); + } + } else { + bundle.installBundle(); + + Jnlp.extensionInstallerService.installSucceeded(false); + } + } catch (Throwable ex) { + logger.error("Error encountered", ex); + + if (hasUI) { + JOptionPane.showMessageDialog( + Installer.getFrame(), + "Installation has failed", + "Installation completion", + JOptionPane.WARNING_MESSAGE); + } + + Jnlp.extensionInstallerService.installFailed(); + } finally { + bundle.close(); + } + } + + //------// + // main // + //------// + /** + * The Installer.main method is called by Java Web Start on two + * occasions within the application cycle: at installation time + * (with single argument {@code install}) and at de-installation + * time (with single argument {@code uninstall}). + * + * @param args either "install" or "uninstall" + * @throws UnavailableServiceException + */ + public static void main (String[] args) + throws UnavailableServiceException + { + if (args.length == 0) { + throw new IllegalArgumentException( + "No argument for Installer." + + " Expecting install or uninstall."); + } + + // Interactive or batch mode? + String mode = System.getProperty("installer-mode", "interactive"); + hasUI = !mode.trim() + .equalsIgnoreCase("batch"); + logger.info("Mode {}.", hasUI ? "interactive" : "batch"); + + Descriptor descriptor = DescriptorFactory.getDescriptor(); + + isInstall = isArgSet("install", args); + isUninstall = isArgSet("uninstall", args); + isAdmin = descriptor.isAdmin(); + + if (isAdmin) { + logger.info("Running as administrator."); + } + + INSTANCE = new Installer(); + + if (isInstall) { + logger.info("Performing install."); + INSTANCE.install(); + } else if (isUninstall) { + logger.info("Performing uninstall."); + INSTANCE.uninstall(); + } else { + // Not a valid argument array + logger.error( + "Illegal Installer arguments: {}", + Arrays.deepToString(args)); + } + } + + //-----------// + // uninstall // + //-----------// + /** + * Launch uninstallation. + */ + public void uninstall () + { + logger.debug("uninstall"); + + try { + bundle.uninstallBundle(); + + if (hasUI) { + JOptionPane.showMessageDialog( + Installer.getFrame(), + "Bundle successfully uninstalled", + "Uninstallation completion", + JOptionPane.INFORMATION_MESSAGE); + } + } catch (Throwable ex) { + if (hasUI) { + JOptionPane.showMessageDialog( + Installer.getFrame(), + "Uninstallation has failed: " + ex, + "Uninstallation completion", + JOptionPane.WARNING_MESSAGE); + } + } finally { + //bundle.close(); + } + } + + //----------// + // isArgSet // + //----------// + /** + * Check whether the provided name appears in the args array. + * + * @param name the name of interest + * @param args the program arguments + * @return true if found, false otherwise + */ + private static boolean isArgSet (String name, + String[] args) + { + for (String arg : args) { + if (arg.equalsIgnoreCase(name)) { + return true; + } + } + + return false; + } +} diff --git a/src/installer/com/audiveris/installer/JarExpander.java b/src/installer/com/audiveris/installer/JarExpander.java new file mode 100644 index 0000000..83fcc28 --- /dev/null +++ b/src/installer/com/audiveris/installer/JarExpander.java @@ -0,0 +1,226 @@ +//----------------------------------------------------------------------------// +// // +// J a r E x p a n d e r // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package com.audiveris.installer; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardCopyOption; +import java.nio.file.attribute.FileTime; +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.List; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; +import java.util.zip.ZipEntry; + +/** + * Class {@code JarExpander} handles the installation of items from a + * .jar archive. + * This is targeted to complement the use of .jar archive for which some + * directories need to be "browsable" and thus expanded as directories and files + * to a given location prior to their browsing. + * + * @author Hervé Bitteur + */ +public class JarExpander +{ + //~ Static fields/initializers --------------------------------------------- + + /** Usual logger utility */ + private static final Logger logger = LoggerFactory.getLogger( + JarExpander.class); + + //~ Instance fields -------------------------------------------------------- + /** Jar file. */ + private final JarFile jar; + + /** Source folder. */ + private final String sourceFolder; + + /** Precise target folder. */ + private final File targetFolder; + + //~ Constructors ----------------------------------------------------------- + //-------------// + // JarExpander // + //-------------// + /** + * Create an JarExpander object for a given folder. + * + * @param jar the jar file to extract data from + * @param sourceFolder name of source folder + * @param targetFolder precise target folder + */ + public JarExpander (final JarFile jar, + final String sourceFolder, + final File targetFolder) + { + this.jar = jar; + this.sourceFolder = sourceFolder; + this.targetFolder = targetFolder; + + logger.debug( + "From jar {} expanding {} entries to folder {}", + jar.getName(), + sourceFolder, + targetFolder); + } + + //~ Methods ---------------------------------------------------------------- + //---------// + // install // + //---------// + /** + * Install, if needed, the desired folder. + * + * @return the number of entries actually written + */ + public int install () + { + // Retrieve source entries in jar file + List sources = getSourceEntries(); + + if (sources.isEmpty()) { + logger.warn("No sources for folder {}", sourceFolder); + + return 0; + } + + // Compare file by file, and update if necessary + int copied = 0; // Number of entries copied + + for (String source : sources) { + copied += process(source); + } + + if (copied != 0) { + logger.info("{} entries copied to {}", copied, targetFolder); + } + + return copied; + } + + //------------------// + // getSourceEntries // + //------------------// + /** + * Retrieve all entries from the jar file that match the folder + * to copy + * + * @return the list of entries found, perhaps empty but not null + */ + private List getSourceEntries () + { + final List found = new ArrayList(); + final Enumeration entries = jar.entries(); + final String prefix = sourceFolder.isEmpty() ? "" + : (sourceFolder.endsWith("/") + ? sourceFolder + : (sourceFolder + "/")); + logger.debug("Prefix is '{}'", prefix); + + // If sourceFolder == "" we take all rooted files, + // excluding the manifest stuff: META-INF/... + while (entries.hasMoreElements()) { + final String entry = entries.nextElement() + .getName(); + + if (prefix.isEmpty()) { + // Just skip the meta inf stuff + if (entry.startsWith("META-INF")) { + logger.trace("Skipping meta-inf {}", entry); + } else { + logger.trace("Found {}", entry); + found.add(entry); + } + } else if (entry.startsWith(prefix)) { + logger.trace("Found {}", entry); + found.add(entry); + } else { + logger.trace("Skipping {}", entry); + } + } + + return found; + } + + //---------// + // process // + //---------// + /** + * Process one entry. + * + * @param source the source entry to check and install + * @return 1 if actually copied, 0 otherwise + */ + private int process (String source) + { + ///logger.debug("Processing source {}", source); + final String sourcePath = sourceFolder.isEmpty() ? source + : source.substring(sourceFolder.length() + 1); + final Path target = Paths.get(targetFolder.toString(), sourcePath); + + try { + if (source.endsWith("/")) { + // This is a directory + if (Files.exists(target)) { + if (!Files.isDirectory(target)) { + logger.warn("Existing non directory {}", target); + } else { + logger.trace("Directory {} exists", target); + } + + return 0; + } else { + Files.createDirectories(target); + logger.trace("Created dir {}", target); + + return 1; + } + } else { + ZipEntry entry = jar.getEntry(source); + + // Target exists? + if (Files.exists(target)) { + // Compare date + FileTime sourceTime = FileTime.fromMillis(entry.getTime()); + FileTime targetTime = Files.getLastModifiedTime(target); + + if (targetTime.compareTo(sourceTime) >= 0) { + logger.trace("Target {} is up to date", target); + + return 0; + } + } + + // Do copy + ///logger.info("About to copy {} to {}", source, target); + try (InputStream is = jar.getInputStream(entry)) { + Files.copy(is, target, StandardCopyOption.REPLACE_EXISTING); + logger.trace("Target {} copied", target); + return 1; + } + } + } catch (IOException ex) { + logger.warn("IOException on " + target, ex); + + return 0; + } + } +} diff --git a/src/installer/com/audiveris/installer/Jnlp.java b/src/installer/com/audiveris/installer/Jnlp.java new file mode 100644 index 0000000..0f70a18 --- /dev/null +++ b/src/installer/com/audiveris/installer/Jnlp.java @@ -0,0 +1,598 @@ +//----------------------------------------------------------------------------// +// // +// J n l p // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package com.audiveris.installer; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URL; + +import javax.jnlp.BasicService; +import javax.jnlp.DownloadService; +import javax.jnlp.DownloadServiceListener; +import javax.jnlp.ExtensionInstallerService; +import javax.jnlp.ServiceManager; +import javax.jnlp.UnavailableServiceException; + +/** + * Class {@code Jnlp} is a facade to JNLP services to allow + * running with and without real JNLP services. + * + * @author Hervé Bitteur + */ +public class Jnlp +{ + //~ Static fields/initializers --------------------------------------------- + + /** Usual logger utility */ + private static final Logger logger = LoggerFactory.getLogger(Jnlp.class); + + /** Extension installer service. */ + public static final ExtensionInstallerService extensionInstallerService = new ExtensionInstallerServiceFacade(); + + /** Basic service. */ + public static final BasicService basicService = new BasicServiceFacade(); + + /** Download service. */ + public static final DownloadService downloadService = new DownloadServiceFacade(); + + //~ Inner Classes ---------------------------------------------------------- + + //--------------------// + // BasicServiceFacade // + //--------------------// + private static class BasicServiceFacade + implements BasicService + { + //~ Instance fields ---------------------------------------------------- + + private BasicService service; + + //~ Constructors ------------------------------------------------------- + + public BasicServiceFacade () + { + try { + service = (BasicService) ServiceManager.lookup( + "javax.jnlp.BasicService"); + logger.debug("Real BasicService available"); + + // Use JNLP cache for all downloads + JnlpResponseCache.init(); + } catch (UnavailableServiceException ex) { + logger.debug("No BasicService available"); + } + } + + //~ Methods ------------------------------------------------------------ + + @Override + public URL getCodeBase () + { + URL res = null; + + if (service != null) { + res = service.getCodeBase(); + } else { + try { + res = new File(".").toURI() + .toURL(); + } catch (MalformedURLException ex) { + logger.error("URL error", ex); + } + } + + logger.debug("getCodeBase => {}", res); + + return res; + } + + @Override + public boolean isOffline () + { + boolean res = false; + + if (service != null) { + res = service.isOffline(); + } + + logger.debug("isOffline => {}", res); + + return res; + } + + @Override + public boolean isWebBrowserSupported () + { + boolean res = false; + + if (service != null) { + res = service.isWebBrowserSupported(); + } + + logger.debug("isWebBrowserSupported => {}", res); + + return res; + } + + @Override + public boolean showDocument (URL url) + { + boolean res = false; + + if (service != null) { + res = service.showDocument(url); + } + + logger.debug("showDocument url: {}, => {}", url, res); + + return res; + } + } + + //-----------------------// + // DownloadServiceFacade // + //-----------------------// + private static class DownloadServiceFacade + implements DownloadService + { + //~ Instance fields ---------------------------------------------------- + + private DownloadService service; + + //~ Constructors ------------------------------------------------------- + + public DownloadServiceFacade () + { + try { + service = (DownloadService) ServiceManager.lookup( + "javax.jnlp.DownloadService"); + logger.debug("Real DownloadService available"); + } catch (UnavailableServiceException ex) { + logger.debug("No DownloadService available"); + } + } + + //~ Methods ------------------------------------------------------------ + + @Override + public DownloadServiceListener getDefaultProgressWindow () + { + DownloadServiceListener res = null; + + if (service != null) { + res = service.getDefaultProgressWindow(); + } + + logger.debug("getDefaultProgressWindow => {}", res); + + return res; + } + + @Override + public boolean isExtensionPartCached (URL url, + String string, + String string1) + { + boolean res = false; + + if (service != null) { + res = service.isExtensionPartCached(url, string, string1); + } + + logger.debug( + "isExtensionPartCached url: {}, string: {}, string1: {} => {}", + url, + string, + string1, + res); + + return res; + } + + @Override + public boolean isExtensionPartCached (URL url, + String string, + String[] strings) + { + boolean res = false; + + if (service != null) { + res = service.isExtensionPartCached(url, string, strings); + } + + logger.debug( + "isExtensionPartCached url: {}, string: {}, strings: {} => {}", + url, + string, + strings, + res); + + return res; + } + + @Override + public boolean isPartCached (String string) + { + boolean res = false; + + if (service != null) { + res = service.isPartCached(string); + } + + logger.debug("isPartCached string: {} => {}", string, res); + + return res; + } + + @Override + public boolean isPartCached (String[] strings) + { + boolean res = false; + + if (service != null) { + res = service.isPartCached(strings); + } + + logger.debug("isPartCached strings: {} => {}", strings, res); + + return res; + } + + @Override + public boolean isResourceCached (URL url, + String string) + { + boolean res = false; + + if (service != null) { + res = service.isResourceCached(url, string); + } + + logger.debug( + "isResourceCached url: {}, string: {} => {}", + url, + string, + res); + + return res; + } + + @Override + public void loadExtensionPart (URL url, + String string, + String string1, + DownloadServiceListener dl) + throws IOException + { + logger.debug( + "loadExtensionPart url: {}, string: {}, string1: {}, dl: {}", + url, + string, + string1, + dl); + + if (service != null) { + service.loadExtensionPart(url, string, string1, dl); + } + } + + @Override + public void loadExtensionPart (URL url, + String string, + String[] strings, + DownloadServiceListener dl) + throws IOException + { + logger.debug( + "loadExtensionPart url: {}, string: {}, strings: {}, dl: {}", + url, + string, + strings, + dl); + + if (service != null) { + service.loadExtensionPart(url, string, strings, dl); + } + } + + @Override + public void loadPart (String string, + DownloadServiceListener dl) + throws IOException + { + logger.debug("loadPart string: {}, dl: {}", string, dl); + + if (service != null) { + service.loadPart(string, dl); + } + } + + @Override + public void loadPart (String[] strings, + DownloadServiceListener dl) + throws IOException + { + logger.debug("loadPart strings: {}, dl: {}", strings, dl); + + if (service != null) { + service.loadPart(strings, dl); + } + } + + @Override + public void loadResource (URL url, + String string, + DownloadServiceListener dl) + throws IOException + { + logger.debug( + "loadResource url: {}, string: {}, dl: {}", + url, + string, + dl); + + if (service != null) { + service.loadResource(url, string, dl); + } + } + + @Override + public void removeExtensionPart (URL url, + String string, + String string1) + throws IOException + { + logger.debug( + "removeExtensionPart url: {}, string: {}, string1: {}", + url, + string, + string1); + + if (service != null) { + service.removeExtensionPart(url, string, string1); + } + } + + @Override + public void removeExtensionPart (URL url, + String string, + String[] strings) + throws IOException + { + logger.debug( + "removeExtensionPart url: {}, string: {}, strings: {}", + url, + string, + strings); + + if (service != null) { + service.removeExtensionPart(url, string, strings); + } + } + + @Override + public void removePart (String string) + throws IOException + { + logger.debug("removePart string: {}", string); + + if (service != null) { + service.removePart(string); + } + } + + @Override + public void removePart (String[] strings) + throws IOException + { + logger.debug("removePart strings: {}", strings.toString()); + + if (service != null) { + service.removePart(strings); + } + } + + @Override + public void removeResource (URL url, + String string) + throws IOException + { + logger.debug("removeResource url: {}, string: {}", url, string); + + if (service != null) { + service.removeResource(url, string); + } + } + } + + //---------------------------------// + // ExtensionInstallerServiceFacade // + //---------------------------------// + private static class ExtensionInstallerServiceFacade + implements ExtensionInstallerService + { + //~ Instance fields ---------------------------------------------------- + + /** Real service, if any. */ + private ExtensionInstallerService service; + + //~ Constructors ------------------------------------------------------- + + public ExtensionInstallerServiceFacade () + { + try { + service = (ExtensionInstallerService) ServiceManager.lookup( + "javax.jnlp.ExtensionInstallerService"); + logger.debug("Real ExtensionInstallerService available"); + + logger.debug("ExtensionLocation = {}", getExtensionLocation()); + logger.debug("InstallPath = {}", getInstallPath()); + } catch (UnavailableServiceException ex) { + logger.debug("No ExtensionInstallerService available"); + } + } + + //~ Methods ------------------------------------------------------------ + + @Override + public URL getExtensionLocation () + { + URL res = null; + + if (service != null) { + res = service.getExtensionLocation(); + } + + logger.debug("getExtensionLocation => {}", res); + + return res; + } + + @Override + public String getExtensionVersion () + { + String res = null; + + if (service != null) { + res = service.getExtensionVersion(); + } + + logger.debug("getExtensionVersion => {}", res); + + return res; + } + + @Override + public String getInstallPath () + { + String res = null; + + if (service != null) { + res = service.getInstallPath(); + } + + logger.debug("getInstallPath => {}", res); + + return res; + } + + @Override + public String getInstalledJRE (URL url, + String string) + { + logger.debug("getInstalledJRE url: {} string: {}", url, string); + + String res = null; + + if (service != null) { + res = service.getInstalledJRE(url, string); + } + + return res; + } + + @Override + public void hideProgressBar () + { + logger.debug("hideProgressBar"); + + if (service != null) { + service.hideProgressBar(); + } + } + + @Override + public void hideStatusWindow () + { + logger.debug("hideStatusWindow"); + + if (service != null) { + service.hideStatusWindow(); + } + } + + @Override + public void installFailed () + { + logger.debug("installFailed"); + + if (service != null) { + service.installFailed(); + } + } + + @Override + public void installSucceeded (boolean reboot) + { + logger.debug("installSucceeded reboot: {}", reboot); + + if (service != null) { + service.installSucceeded(reboot); + } + } + + @Override + public void setHeading (String string) + { + logger.debug("setHeading string: {}", string); + + if (service != null) { + service.setHeading(string); + } + } + + @Override + public void setJREInfo (String string, + String string1) + { + logger.debug("setJREInfo string: {} string1: {}", string, string1); + + if (service != null) { + service.setJREInfo(string, string1); + } + } + + @Override + public void setNativeLibraryInfo (String string) + { + logger.debug("setNativeLibraryInfo string: {}", string); + + if (service != null) { + service.setNativeLibraryInfo(string); + } + } + + @Override + public void setStatus (String string) + { + logger.debug("setStatus string: {}", string); + + if (service != null) { + service.setStatus(string); + } + } + + @Override + public void updateProgress (int i) + { + logger.debug("updateProgress i: {}", i); + + if (service != null) { + service.updateProgress(i); + } + } + } +} diff --git a/src/installer/com/audiveris/installer/JnlpResponseCache.java b/src/installer/com/audiveris/installer/JnlpResponseCache.java new file mode 100644 index 0000000..c391c47 --- /dev/null +++ b/src/installer/com/audiveris/installer/JnlpResponseCache.java @@ -0,0 +1,93 @@ +//----------------------------------------------------------------------------// +// // +// J n l p R e s p o n s e C a c h e // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package com.audiveris.installer; + +import java.io.IOException; +import java.net.CacheRequest; +import java.net.CacheResponse; +import java.net.ResponseCache; +import java.net.URI; +import java.net.URL; +import java.net.URLConnection; +import java.util.List; +import java.util.Map; + +import javax.jnlp.DownloadService; +import javax.jnlp.ServiceManager; +import javax.jnlp.UnavailableServiceException; + +/** + * Class {@code JnlpResponseCache} enables the Java Cache for JNLP. + * You need to call once JnlpResponseCache.init() in your code to enable it. + * + * @author horcrux7 http://stackoverflow.com/users/12631/horcrux7 + */ +public class JnlpResponseCache + extends ResponseCache +{ + //~ Instance fields -------------------------------------------------------- + + private final DownloadService service; + + //~ Constructors ----------------------------------------------------------- + + //-------------------// + // JnlpResponseCache // + //-------------------// + private JnlpResponseCache () + { + try { + service = (DownloadService) ServiceManager.lookup( + "javax.jnlp.DownloadService"); + } catch (UnavailableServiceException ex) { + throw new NoClassDefFoundError(ex.toString()); + } + } + + //~ Methods ---------------------------------------------------------------- + + //---------------// + // CacheResponse // + //---------------// + @Override + public CacheResponse get (URI uri, + String rqstMethod, + Map> rqstHeaders) + throws IOException + { + return null; + } + + //--------------// + // CacheRequest // + //--------------// + @Override + public CacheRequest put (URI uri, + URLConnection conn) + throws IOException + { + URL url = uri.toURL(); + service.loadResource(url, null, service.getDefaultProgressWindow()); + + return null; + } + + //------// + // init // + //------// + static void init () + { + if (ResponseCache.getDefault() == null) { + ResponseCache.setDefault(new JnlpResponseCache()); + } + } +} diff --git a/src/installer/com/audiveris/installer/LangSelector.java b/src/installer/com/audiveris/installer/LangSelector.java new file mode 100644 index 0000000..990c534 --- /dev/null +++ b/src/installer/com/audiveris/installer/LangSelector.java @@ -0,0 +1,400 @@ +//----------------------------------------------------------------------------// +// // +// L a n g S e l e c t o r // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package com.audiveris.installer; + +import com.jgoodies.forms.builder.PanelBuilder; +import com.jgoodies.forms.factories.CC; +import com.jgoodies.forms.layout.CellConstraints; +import com.jgoodies.forms.layout.FormLayout; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.awt.Color; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.MouseEvent; +import java.awt.event.MouseListener; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Set; +import java.util.TreeSet; + +import javax.swing.AbstractAction; +import javax.swing.JButton; +import javax.swing.JComponent; +import javax.swing.JList; +import javax.swing.JMenuItem; +import javax.swing.JOptionPane; +import javax.swing.JPanel; +import javax.swing.JPopupMenu; +import javax.swing.JScrollPane; + +/** + * Class {@code LangSelector} handles the collection of OCR languages, + * displays a banner of relevant languages, with the ability + * to add or remove languages. + *

+ * If no language is desired, bundle installation cannot be launched. + * + * @author Hervé Bitteur + */ +public class LangSelector +{ + //~ Static fields/initializers --------------------------------------------- + + /** Usual logger utility */ + private static final Logger logger = LoggerFactory.getLogger( + LangSelector.class); + + //~ Instance fields -------------------------------------------------------- + + /** Related companion. */ + private final OcrCompanion companion; + + /** Component. */ + private JPanel component; + + /** Display of handled languages. */ + private Banner banner; + + /** Desired languages. */ + private Set desired; + + /** Non-desired languages. */ + private Set nonDesired; + + //~ Constructors ----------------------------------------------------------- + + //--------------// + // LangSelector // + //--------------// + /** + * Creates a new LangSelector object. + * + * @param companion the language companion + */ + public LangSelector (OcrCompanion companion) + { + this.companion = companion; + + desired = companion.getDesired(); + nonDesired = companion.getNonDesired(); + + component = defineLayout(); + } + + //~ Methods ---------------------------------------------------------------- + + //--------------// + // getComponent // + //--------------// + public JPanel getComponent () + { + return component; + } + + //--------// + // update // + //--------// + /** + * Update the banner display. + */ + public void update (final String currentLang) + { + // It's easier to merely rebuild the banner component + banner.defineLayout(currentLang); + } + + //--------------// + // defineLayout // + //--------------// + private JPanel defineLayout () + { + final JPanel comp = new JPanel(); + final FormLayout layout = new FormLayout( + "right:40dlu, $lcgap, fill:0:grow, $lcgap, 33dlu", + "pref"); + final CellConstraints cst = new CellConstraints(); + final PanelBuilder builder = new PanelBuilder(layout, comp); + + // Label on left side + builder.addROLabel("Languages", cst.xy(1, 1)); + + // "Banner" for the center of the line + banner = new Banner(); + builder.add(banner.getComponent(), cst.xy(3, 1)); + + // "Add" button on right side + JButton button = new JButton(new AddAction()); + builder.add(button, cst.xy(5, 1)); + + return comp; + } + + //~ Inner Classes ---------------------------------------------------------- + + //-----------// + // AddAction // + //-----------// + /** Addition of one or several languages. */ + private class AddAction + extends AbstractAction + { + //~ Constructors ------------------------------------------------------- + + public AddAction () + { + putValue(AbstractAction.NAME, "Add"); + putValue( + AbstractAction.SHORT_DESCRIPTION, + "Add one or several languages"); + } + + //~ Methods ------------------------------------------------------------ + + @Override + public void actionPerformed (ActionEvent e) + { + // Create a dialog with a JList of possible additions + // That is all languages, minus those already desired + List additionals = new ArrayList( + Arrays.asList(OcrCompanion.ALL_LANGUAGES)); + additionals.removeAll(desired); + + JList list = new JList( + additionals.toArray(new String[additionals.size()])); + JScrollPane scrollPane = new JScrollPane(list); + list.setLayoutOrientation(JList.VERTICAL_WRAP); + list.setVisibleRowCount(10); + + // Let the user select additional languages + int opt = JOptionPane.showConfirmDialog( + Installer.getFrame(), + scrollPane, + "OCR languages selection", + JOptionPane.OK_CANCEL_OPTION, + JOptionPane.QUESTION_MESSAGE); + List toAdd = list.getSelectedValuesList(); + logger.debug("Opt: {} Selection: {}", opt, toAdd); + + // Save the selection, only if OK + if (opt == JOptionPane.OK_OPTION) { + logger.info("Additional languages: {}", toAdd); + desired.addAll(toAdd); + + // This may impact the "installed" status of the companion + companion.checkInstalled(); + banner.defineLayout(null); + companion.updateView(); + } + } + } + + //--------// + // Banner // + //--------// + /** + * The banner displays all relevant languages with their status. + * desired / installed : green, nothing to do + * desired / not installed : pink, installation scheduled + * not desired / installed : gray, removal scheduled + * not desired / not installed : not relevant, nothing to do + */ + private class Banner + implements ActionListener + { + //~ Instance fields ---------------------------------------------------- + + private final JPanel panel = new JPanel(); + private final JScrollPane scrollPane = new JScrollPane(panel); + + //~ Constructors ------------------------------------------------------- + + public Banner () + { + panel.setBorder(null); + scrollPane.setBorder(null); + scrollPane.setVerticalScrollBarPolicy( + JScrollPane.VERTICAL_SCROLLBAR_NEVER); + + defineLayout(null); + } + + //~ Methods ------------------------------------------------------------ + + /** Triggered by popup selection. */ + @Override + public void actionPerformed (ActionEvent e) + { + // Remove the designated language + final JMenuItem item = (JMenuItem) e.getSource(); + final String lang = item.getName(); + logger.debug("del lang: {}", lang); + + desired.remove(lang); + + if (companion.isLangInstalled(lang)) { + nonDesired.add(lang); + logger.info("Language {} to be removed", lang); + } + + // This may impact the "installed" status of the companion + companion.checkInstalled(); + banner.defineLayout(null); + companion.updateView(); + } + + public final void defineLayout (final String currentLang) + { + panel.removeAll(); + + final Set relevant = new TreeSet(); + relevant.addAll(desired); + relevant.addAll(nonDesired); + + final String gap = "$lcgap"; + final StringBuilder columns = new StringBuilder(); + + for (int i = 0; i < relevant.size(); i++) { + if (columns.length() > 0) { + columns.append(","); + } + + columns.append(gap) + .append(",") + .append("pref"); + } + + final CellConstraints cst = new CellConstraints(); + final FormLayout layout = new FormLayout( + columns.toString(), + "center:16dlu"); + final PanelBuilder builder = new PanelBuilder(layout, panel); + PanelBuilder.setOpaqueDefault(true); + builder.background(Color.WHITE); + + int col = 2; + + for (String lang : relevant) { + final FormLayout langLayout = new FormLayout( + "$lcgap,center:pref,$lcgap", + "center:12dlu"); + final JPanel comp = new JPanel(); + comp.setBackground(getBackground(lang, currentLang)); + + final PanelBuilder langBuilder = new PanelBuilder( + langLayout, + comp); + langBuilder.addROLabel(lang, CC.xy(2, 1)); + comp.addMouseListener(createPopupListener(lang)); + comp.setToolTipText("Use right-click to remove this language"); + + builder.add(comp, cst.xy(col, 1)); + col += 2; + } + + panel.revalidate(); + panel.repaint(); + } + + public JComponent getComponent () + { + return scrollPane; + } + + protected Color getBackground (final String language, + final String current) + { + if (language.equals(current)) { + return CompanionView.COLORS.BEING; + } + + if (companion.isLangInstalled(language)) { + if (desired.contains(language)) { + return CompanionView.COLORS.INST; + } else { + return CompanionView.COLORS.UNUSED; + } + } else { + return CompanionView.COLORS.NOT_INST; + } + } + + private MouseListener createPopupListener (String lang) + { + // Create a very simple popup menu. + final JPopupMenu popup = new JPopupMenu(); + final JMenuItem menuItem = new JMenuItem("Remove " + lang); + menuItem.setName(lang); + menuItem.addActionListener(this); + popup.add(menuItem); + + return new PopupListener(popup); + } + } + + //---------------// + // PopupListener // + //---------------// + private class PopupListener + implements MouseListener + { + //~ Instance fields ---------------------------------------------------- + + JPopupMenu popup; + + //~ Constructors ------------------------------------------------------- + + PopupListener (JPopupMenu popupMenu) + { + popup = popupMenu; + } + + //~ Methods ------------------------------------------------------------ + + @Override + public void mouseClicked (MouseEvent e) + { + } + + @Override + public void mouseEntered (MouseEvent e) + { + } + + @Override + public void mouseExited (MouseEvent e) + { + } + + @Override + public void mousePressed (MouseEvent e) + { + maybeShowPopup(e); + } + + @Override + public void mouseReleased (MouseEvent e) + { + maybeShowPopup(e); + } + + private void maybeShowPopup (MouseEvent e) + { + if (e.isPopupTrigger()) { + popup.show(e.getComponent(), e.getX(), e.getY()); + } + } + } +} diff --git a/src/installer/com/audiveris/installer/LicenseCompanion.java b/src/installer/com/audiveris/installer/LicenseCompanion.java new file mode 100644 index 0000000..0bdc498 --- /dev/null +++ b/src/installer/com/audiveris/installer/LicenseCompanion.java @@ -0,0 +1,187 @@ +//----------------------------------------------------------------------------// +// // +// L i c e n s e C o m p a n i o n // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package com.audiveris.installer; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.beans.PropertyChangeEvent; +import java.beans.PropertyChangeListener; +import java.net.MalformedURLException; +import java.net.URL; + +import javax.swing.JDialog; +import javax.swing.JOptionPane; + +/** + * Class {@code LicenseCompanion} checks user agreement WRT license. + * + * @author Hervé Bitteur + */ +public class LicenseCompanion + extends AbstractCompanion +{ + //~ Static fields/initializers --------------------------------------------- + + /** Usual logger utility */ + private static final Logger logger = LoggerFactory.getLogger( + LicenseCompanion.class); + + private static final String DESC = "This ensures you agree with Audiveris license." + + "
(You will be able to browse the license text)"; + + /** Name for license. */ + private static final String LICENSE_NAME = "GNU GPL V2"; + + /** URL for license browsing. */ + private static final String LICENSE_URL = "http://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html"; + + //~ Constructors ----------------------------------------------------------- + //------------------// + // LicenseCompanion // + //------------------// + /** + * Creates a new LicenseCompanion object. + */ + public LicenseCompanion () + { + super("License", DESC); + + if (Installer.hasUI()) { + view = new BasicCompanionView(this, 70); + } + } + + //~ Methods ---------------------------------------------------------------- + //----------------// + // checkInstalled // + //----------------// + @Override + public boolean checkInstalled () + { + return status == Status.INSTALLED; + } + + //-----------// + // doInstall // + //-----------// + @Override + protected void doInstall () + throws Exception + { + // When running without UI, we assume license is accepted + if (!Installer.hasUI()) { + return; + } + + // User choice (must be an output, yet final) + final boolean[] isOk = new boolean[1]; + + final String yes = "Yes"; + final String no = "No"; + final String browse = "View License"; + final JOptionPane optionPane = new JOptionPane( + "Do you agree to license " + LICENSE_NAME + "?", + JOptionPane.QUESTION_MESSAGE, + JOptionPane.YES_NO_CANCEL_OPTION, + null, + new Object[]{yes, no, browse}, + yes); + final String frameTitle = "End User License Agreement"; + final JDialog dialog = new JDialog( + Installer.getFrame(), + frameTitle, + true); + dialog.setContentPane(optionPane); + + // Prevent dialog closing + dialog.setDefaultCloseOperation(JDialog.DO_NOTHING_ON_CLOSE); + + optionPane.addPropertyChangeListener( + new PropertyChangeListener() + { + @Override + public void propertyChange (PropertyChangeEvent e) + { + String prop = e.getPropertyName(); + + if (dialog.isVisible() + && (e.getSource() == optionPane) + && (prop.equals(JOptionPane.VALUE_PROPERTY))) { + Object option = optionPane.getValue(); + logger.debug("option: {}", option); + + if (option == yes) { + isOk[0] = true; + dialog.setVisible(false); + dialog.dispose(); + } else if (option == no) { + isOk[0] = false; + dialog.setVisible(false); + dialog.dispose(); + } else if (option == browse) { + logger.info( + "Launching browser on {}", + LICENSE_URL); + showLicense(); + optionPane.setValue( + JOptionPane.UNINITIALIZED_VALUE); + } else { + } + } + } + }); + + dialog.pack(); + dialog.setLocationRelativeTo(Installer.getFrame()); + dialog.setVisible(true); + + logger.debug("OK: {}", isOk[0]); + + if (!isOk[0]) { + throw new LicenseDeclinedException(); + } + } + + //-------------// + // showLicense // + //-------------// + private void showLicense () + { + try { + final URL url = new URL(LICENSE_URL); + + if (Jnlp.basicService.isWebBrowserSupported()) { + Jnlp.basicService.showDocument(url); + } else { + logger.warn("Browser is not supported"); + } + } catch (MalformedURLException ex) { + logger.error("Malformed URL " + LICENSE_URL, ex); + } + } + + //~ Inner Classes ---------------------------------------------------------- + //--------------------------// + // LicenseDeclinedException // + //--------------------------// + public static class LicenseDeclinedException + extends RuntimeException + { + //~ Constructors ------------------------------------------------------- + + public LicenseDeclinedException () + { + super("License declined by user"); + } + } +} diff --git a/src/installer/com/audiveris/installer/LogUtilities.java b/src/installer/com/audiveris/installer/LogUtilities.java new file mode 100644 index 0000000..ec03a8d --- /dev/null +++ b/src/installer/com/audiveris/installer/LogUtilities.java @@ -0,0 +1,161 @@ +//----------------------------------------------------------------------------// +// // +// L o g U t i l i t i e s // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package com.audiveris.installer; + +import ch.qos.logback.classic.Level; +import ch.qos.logback.classic.Logger; +import ch.qos.logback.classic.LoggerContext; +import ch.qos.logback.classic.encoder.PatternLayoutEncoder; +import ch.qos.logback.classic.spi.LoggingEvent; +import ch.qos.logback.core.Appender; +import ch.qos.logback.core.ConsoleAppender; +import ch.qos.logback.core.FileAppender; +import ch.qos.logback.core.filter.Filter; +import ch.qos.logback.core.spi.FilterReply; +import ch.qos.logback.core.util.StatusPrinter; + +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.nio.file.Paths; +import java.text.SimpleDateFormat; +import java.util.Date; + +/** + * Class {@code LogUtilities} handles default logging configuration + * for the Installer. + * + * @author Hervé Bitteur + */ +public class LogUtilities +{ + //~ Static fields/initializers --------------------------------------------- + + /** System property for LogBack configuration. */ + private static final String LOGBACK_LOGGING_KEY = "logback.configurationFile"; + + //~ Methods ---------------------------------------------------------------- + //------------// + // initialize // + //------------// + /** + * Check for (BackLog) logging configuration, and if not found, + * define a minimal configuration. + * This method should be called at the very beginning of the program before + * any logging request is sent. + */ + public static void initialize () + { + // Check if system property is set and points to a real file + final String loggingProp = System.getProperty(LOGBACK_LOGGING_KEY); + + if (loggingProp != null) { + File loggingFile = new File(loggingProp); + + if (loggingFile.exists()) { + // Everything seems OK, let LogBack use the config file + System.out.println("Using " + loggingFile.getAbsolutePath()); + + return; + } else { + System.out.println( + "File " + loggingFile.getAbsolutePath() + + " does not exist."); + } + } else { + System.out.println( + "Property " + LOGBACK_LOGGING_KEY + " not defined."); + } + + // Define a minimal logging configuration, programmatically + LoggerContext loggerContext = (LoggerContext) LoggerFactory.getILoggerFactory(); + Logger root = (ch.qos.logback.classic.Logger) LoggerFactory.getLogger( + Logger.ROOT_LOGGER_NAME); + + // CONSOLE + ConsoleAppender consoleAppender = new ConsoleAppender(); + PatternLayoutEncoder consoleEncoder = new PatternLayoutEncoder(); + consoleAppender.setName("CONSOLE"); + consoleAppender.setContext(loggerContext); + consoleEncoder.setContext(loggerContext); + consoleEncoder.setPattern("%-5level %caller{1} - %msg%ex%n"); + consoleEncoder.start(); + consoleAppender.setEncoder(consoleEncoder); + consoleAppender.start(); + root.addAppender(consoleAppender); + + // FILE (located in default temp directory) + File logFile; + FileAppender fileAppender = new FileAppender(); + PatternLayoutEncoder fileEncoder = new PatternLayoutEncoder(); + fileAppender.setName("FILE"); + fileAppender.setContext(loggerContext); + fileAppender.setAppend(false); + + String now = new SimpleDateFormat("yyyyMMdd'T'HHmmss").format( + new Date()); + logFile = Paths.get( + System.getProperty("java.io.tmpdir"), + "audiveris-installer-" + now + ".log") + .toFile(); + fileAppender.setFile(logFile.getAbsolutePath()); + fileEncoder.setContext(loggerContext); + fileEncoder.setPattern("%date %level \\(%file:%line\\) - %msg%ex%n"); + fileEncoder.start(); + fileAppender.setEncoder(fileEncoder); + fileAppender.start(); + root.addAppender(fileAppender); + + // VIEW (filtered on INFO+) + Appender guiAppender = new ViewAppender(); + guiAppender.setName("VIEW"); + guiAppender.setContext(loggerContext); + + Filter filter = new Filter() + { + @Override + public FilterReply decide (Object obj) + { + if (!isStarted()) { + return FilterReply.NEUTRAL; + } + + LoggingEvent event = (LoggingEvent) obj; + + if (event.getLevel() + .toInt() >= Level.INFO_INT) { + return FilterReply.NEUTRAL; + } else { + return FilterReply.DENY; + } + } + }; + + filter.start(); + + guiAppender.addFilter(filter); + guiAppender.start(); + root.addAppender(guiAppender); + + // Levels + root.setLevel(Level.DEBUG); + + Logger jarExpander = (ch.qos.logback.classic.Logger) LoggerFactory.getLogger( + JarExpander.class); + jarExpander.setLevel(Level.INFO); + + // OPTIONAL: print logback internal status messages + StatusPrinter.print(loggerContext); + + root.info("Logging to file {}", logFile.getAbsolutePath()); + } +} diff --git a/src/installer/com/audiveris/installer/MessagePanel.java b/src/installer/com/audiveris/installer/MessagePanel.java new file mode 100644 index 0000000..4d3c942 --- /dev/null +++ b/src/installer/com/audiveris/installer/MessagePanel.java @@ -0,0 +1,130 @@ +//----------------------------------------------------------------------------// +// // +// M e s s a g e P a n e l // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package com.audiveris.installer; + +import ch.qos.logback.classic.Level; + +import java.awt.Color; +import java.awt.Insets; + +import javax.swing.JScrollPane; +import javax.swing.JTextPane; +import javax.swing.SwingUtilities; +import javax.swing.text.AbstractDocument; +import javax.swing.text.BadLocationException; +import javax.swing.text.SimpleAttributeSet; +import javax.swing.text.StyleConstants; + +/** + * Class {@code MessagePanel} handles a panel meant to display log + * messages. + * + * @author Hervé Bitteur + */ +public class MessagePanel +{ + //~ Static fields/initializers --------------------------------------------- + + private static final String LOG_FONT = "Lucida Console"; + + //~ Instance fields -------------------------------------------------------- + + private final JTextPane textPane = new JTextPane(); + private final AbstractDocument document = (AbstractDocument) textPane.getStyledDocument(); + private final SimpleAttributeSet attributes = new SimpleAttributeSet(); + + /** Host the text in a scroll pane. */ + private JScrollPane panel = new JScrollPane(); + + //~ Constructors ----------------------------------------------------------- + + /** + * Creates a new MessagePanel object. + */ + public MessagePanel () + { + textPane.setEditable(false); + textPane.setMargin(new Insets(5, 5, 5, 5)); + panel.setViewportView(textPane); + + // Font name & size + StyleConstants.setFontFamily(attributes, LOG_FONT); + StyleConstants.setFontSize(attributes, 10); + } + + //~ Methods ---------------------------------------------------------------- + + //----------// + // clearLog // + //----------// + /** + * Clear the display. + */ + public void clear () + { + textPane.setText(""); + textPane.setCaretPosition(0); + panel.repaint(); + } + + //---------// + // display // + //---------// + public void display (final Level level, + final String str) + { + SwingUtilities.invokeLater( + new Runnable() { + @Override + public void run () + { + // Color + StyleConstants.setForeground( + attributes, + getLevelColor(level)); + + try { + document.insertString( + document.getLength(), + str + "\n", + attributes); + } catch (BadLocationException ex) { + ex.printStackTrace(); + } + } + }); + } + + //--------------// + // getComponent // + //--------------// + public JScrollPane getComponent () + { + return panel; + } + + //---------------// + // getLevelColor // + //---------------// + private Color getLevelColor (Level level) + { + if (level.isGreaterOrEqual(Level.ERROR)) { + return Color.RED; + } else if (level.isGreaterOrEqual(Level.WARN)) { + return Color.BLUE; + } else if (level.isGreaterOrEqual(Level.INFO)) { + return Color.BLACK; + } else { + return Color.GRAY; + } + } +} diff --git a/src/installer/com/audiveris/installer/OcrCompanion.java b/src/installer/com/audiveris/installer/OcrCompanion.java new file mode 100644 index 0000000..0903af4 --- /dev/null +++ b/src/installer/com/audiveris/installer/OcrCompanion.java @@ -0,0 +1,499 @@ +//----------------------------------------------------------------------------// +// // +// O c r C o m p a n i o n // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package com.audiveris.installer; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.io.IOException; +import java.nio.file.AccessDeniedException; +import java.nio.file.FileVisitOption; +import java.nio.file.FileVisitResult; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.SimpleFileVisitor; +import java.nio.file.attribute.BasicFileAttributes; +import java.util.Arrays; +import java.util.EnumSet; +import java.util.Set; +import java.util.TreeSet; + +import javax.swing.JOptionPane; + +/** + * Class {@code OcrCompanion} handles the installation of Tesseract + * library and the support for a selected set of languages. + * + * @author Hervé Bitteur + */ +public class OcrCompanion + extends AbstractCompanion +{ + //~ Static fields/initializers --------------------------------------------- + + /** Usual logger utility */ + private static final Logger logger = LoggerFactory.getLogger( + OcrCompanion.class); + + /** Companion description. */ + private static final String DESC = "You can select which precise languages" + + "
the OCR should support." + + "
For this, please use the language selector below."; + + /** Environment descriptor. */ + private static final Descriptor descriptor = DescriptorFactory.getDescriptor(); + + /** Internet address to retrieve Tesseract trained data. */ + private static final String TESS_RADIX = "http://tesseract-ocr.googlecode.com/files"; + + /** Precise Tesseract version. */ + private static final String TESS_VERSION = "tesseract-ocr-3.02"; + + /** + * Collection of all Tesseract supported languages. + * Update this list according to Tesseract web site on + * http://code.google.com/p/tesseract-ocr/downloads/list + */ + public static final String[] ALL_LANGUAGES = new String[]{ + "afr", "ara", "aze", "bel", + "ben", "bul", "cat", "ces", + "chi_sim", "chi_tra", "chr", + "dan", "deu", "ell", "eng", + "enm", "epo", "epo_alt", + "equ", "est", "eus", "fin", + "fra", "frk", "frm", "glg", + "grc", "heb", "hin", "hrv", + "hun", "ind", "isl", "ita", + "ita_old", "jpn", "kan", + "kor", "lav", "lit", "mal", + "mkd", "mlt", "msa", "nld", + "nor", "pol", "por", "ron", + "rus", "slk", "slv", "spa", + "spa_old", "sqi", "srp", + "swa", "swe", "tam", "tel", + "tgl", "tha", "tur", "ukr", + "vie" + }; + + /** Collection of pre-desired languages. */ + public static final String[] PREDESIRED_LANGUAGES = new String[]{ + "deu", "eng", "fra", + "ita" + }; + + /** Name of local temporary folder for OCR languages. */ + public static final String LOCAL_OCR_FOLDER = "local-ocr-folder"; + + /** Name of local temporary folder for binary files. */ + public static final String LOCAL_LIB_FOLDER = "local-lib-folder"; + + //~ Instance fields -------------------------------------------------------- + /** Handling of tessdata directory. */ + private final Tessdata tessdata = new Tessdata(); + + /** The languages to be added (if not present). */ + private final Set desired = buildDesiredLanguages(); + + /** The languages to be removed (if present). */ + private final Set nonDesired = new TreeSet(); + + /** User selector, if any. */ + private LangSelector selector; + + //~ Constructors ----------------------------------------------------------- + //--------------// + // OcrCompanion // + //--------------// + /** + * Creates a new OcrCompanion object for OCR library and a list of + * supported languages. + */ + public OcrCompanion () + { + super("OCR", DESC); + + if (Installer.hasUI()) { + view = new BasicCompanionView(this, 60); + selector = new LangSelector(this); + } + } + + //~ Methods ---------------------------------------------------------------- + //----------------// + // checkInstalled // + //----------------// + @Override + public boolean checkInstalled () + { + try { + boolean installed = true; + + // Check for Tesseract library + if (!descriptor.isTesseractInstalled()) { + installed = false; + } else { + if (selector != null) { + selector.update(null); + } + + // We check if each of the desired language actually exists + for (String language : desired) { + if (!isLangInstalled(language)) { + installed = false; + + break; + } + } + + if (installed) { + // We check if each of the non-desired language still exists + for (String language : nonDesired) { + if (isLangInstalled(language)) { + installed = false; + + break; + } + } + } + } + + status = installed ? Status.INSTALLED : Status.NOT_INSTALLED; + } catch (Throwable ex) { + logger.warn("Tesseract could not be checked", ex); + status = Status.NOT_INSTALLED; + } + + return status == Status.INSTALLED; + } + + //------------// + // getDesired // + //------------// + /** + * Report the set of desired languages. + * + * @return the current set of desired languages + */ + public Set getDesired () + { + return desired; + } + + //------------------// + // getInstallWeight // + //------------------// + @Override + public int getInstallWeight () + { + return isNeeded() ? 1 : 0; + } + + //---------------// + // getNonDesired // + //---------------// + /** + * Report the set of non-desired languages. + * + * @return the current set of non-desired languages + */ + public Set getNonDesired () + { + return nonDesired; + } + + //-------------// + // getSelector // + //-------------// + public LangSelector getSelector () + { + return selector; + } + + //-----------------// + // isLangInstalled // + //-----------------// + public boolean isLangInstalled (String language) + { + try { + // We check if the main language file actually exists + final File lanFile = new File( + tessdata.get(), + language + ".traineddata"); + + return lanFile.exists(); + } catch (Exception ex) { + logger.warn("No tessdata found", ex); + + return false; + } + } + + //-----------// + // doInstall // + //-----------// + @Override + protected void doInstall () + throws Exception + { + // First, install library if so needed + if (!descriptor.isTesseractInstalled()) { + descriptor.installTesseract(); + } + + // Then, we process languages in alphabetical order + // To keep in sync with selector display + final Set relevant = new TreeSet(); + relevant.addAll(desired); + relevant.addAll(nonDesired); + + for (String lang : relevant) { + if (desired.contains(lang) && !isLangInstalled(lang)) { + if (selector != null) { + selector.update(lang); + } + + installLanguage(lang); + } else if (nonDesired.contains(lang)) { + if (selector != null) { + selector.update(lang); + } + + uninstallLanguage(lang); + nonDesired.remove(lang); + } + + if (selector != null) { + selector.update(null); + } + } + + // If some languages have been installed, copy them to tessdata + final File local = getLocalOcrFolder(); + + if (local.exists() + && local.isDirectory() + && (local.listFiles().length > 0)) { + // Try immediate copy, in user mode + final Path[] sources = new Path[]{ + new File( + local, + Descriptor.TESSERACT_OCR).toPath() + }; + final Path target = tessdata.get() + .getParentFile() + .getParentFile() + .toPath(); + + try { + FileCopier fc = new FileCopier(sources, target, true); + fc.copy(); + } catch (IOException ex) { + logger.debug( + "Could not directly copy language files" + + ", will post a copy command at system level"); + + // Fallback to a posted copy command + // Dirty hack since XCOPY (Windows) and cp (Unix) don't treat + // source and target similarly when these are directories + Path source = null; + + if (DescriptorFactory.WINDOWS) { + source = local.toPath(); + } else if (DescriptorFactory.LINUX) { + source = new File(local, Descriptor.TESSERACT_OCR).toPath(); + } + + // Here, source and target are folders (not regular files) + appendCommand(descriptor.getCopyCommand(source, target)); + } + } + } + + //-----------------------// + // buildDesiredLanguages // + //-----------------------// + private Set buildDesiredLanguages () + { + Set set = new TreeSet(); + + // Initialize selected languages with the pre-selected ones + set.addAll(Arrays.asList(PREDESIRED_LANGUAGES)); + + // Include all the already installed languages as well + for (String lang : ALL_LANGUAGES) { + if (isLangInstalled(lang)) { + set.add(lang); + } + } + + return set; + } + + //-------------------// + // getLocalOcrFolder // + //-------------------// + /** + * Report the local (temporary) folder where OCR language data are + * expanded before being copied to final target location. + * + * @return the local OCR folder + */ + private File getLocalOcrFolder () + { + return new File(descriptor.getTempFolder(), LOCAL_OCR_FOLDER); + } + + //-----------------// + // installLanguage // + //-----------------// + private void installLanguage (final String lang) + throws Exception + { + try { + final String tarName = TESS_VERSION + "." + lang + ".tar"; + final String archiveName = tarName + ".gz"; + final String archiveHttp = TESS_RADIX + "/" + archiveName; + + // Download + final File temp = descriptor.getTempFolder(); + final File targz = new File(temp, archiveName); + Utilities.download(archiveHttp, targz); + + // Decompress the .tar.gz + Expander.unGzip(targz, temp); + + // Expand the .tar to LOCAL_OCR_FOLDER + final File tar = new File(temp, tarName); + logger.debug("tar: {}", tar); + + final File local = getLocalOcrFolder(); + final File data = new File( + new File(local, Descriptor.TESSERACT_OCR), + Descriptor.TESSDATA); + data.mkdirs(); + logger.debug("local tessdata folder: {}", data.getAbsolutePath()); + Expander.unTar(tar, local); + } catch (Exception ex) { + JOptionPane.showMessageDialog( + Installer.getFrame(), + ex.getMessage()); + throw ex; + } + } + + //-------------------// + // uninstallLanguage // + //-------------------// + private void uninstallLanguage (final String lang) + throws Exception + { + if (!tessdata.get() + .exists()) { + return; + } + + // Clean up relevant files in the folder + Files.walkFileTree( + tessdata.get().toPath(), + EnumSet.noneOf(FileVisitOption.class), + 1, + new SimpleFileVisitor() + { + @Override + public FileVisitResult visitFile (Path file, + BasicFileAttributes attrs) + throws IOException + { + Path name = file.getName(file.getNameCount() - 1); + logger.debug("Visiting {}, name {}", file, name); + + if (name.toString() + .startsWith(lang + ".")) { + logger.info("Removing file {}", file); + + try { + // Try immediate delete + Files.delete(file); + } catch (AccessDeniedException ex) { + logger.info( + "Cannot delete {}, will post a delete command", + file); + + // Post a delete command + appendCommand( + descriptor.getDeleteCommand(file)); + } + } + + return FileVisitResult.CONTINUE; + } + }); + } + + //~ Inner Classes ---------------------------------------------------------- + //----------// + // Tessdata // + //----------// + /** + * Handles the precise location of tessdata. + */ + private static class Tessdata + { + //~ Instance fields ---------------------------------------------------- + + private File prefixDir; + + private File tessdataDir; + + //~ Methods ------------------------------------------------------------ + /** + * Report tessdata directory specification (there is no + * guarantee that the directory actually exists). + * If environment variable TESSDATA_PREFIX exists, it is used as the + * path to tessdata parent directory. + * Otherwise, we use the OS-dependent default prefix. + * + * @return the tessdata directory + */ + public File get () + { + if (tessdataDir == null) { + tessdataDir = new File(getPrefix(), Descriptor.TESSDATA); + } + + return tessdataDir; + } + + /** + * Report the parent folder of tessdata, usually the one + * pointed to by TESSDATA_PREFIX (there is no guarantee that + * the directory exists or is writable). + * + * @return the tessdata PARENT directory + */ + public File getPrefix () + { + if (prefixDir == null) { + final String prefix = System.getenv(Descriptor.TESSDATA_PREFIX); + + if (prefix == null) { + prefixDir = descriptor.getDefaultTessdataPrefix(); + } else { + prefixDir = new File(prefix); + } + } + + return prefixDir; + } + } +} diff --git a/src/installer/com/audiveris/installer/PluginsCompanion.java b/src/installer/com/audiveris/installer/PluginsCompanion.java new file mode 100644 index 0000000..ab5e127 --- /dev/null +++ b/src/installer/com/audiveris/installer/PluginsCompanion.java @@ -0,0 +1,91 @@ +//----------------------------------------------------------------------------// +// // +// P l u g i n s C o m p a n i o n // +// // +//----------------------------------------------------------------------------// +// // +// Copyright © Herve 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. // +//----------------------------------------------------------------------------// +// +package com.audiveris.installer; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.net.URI; +import java.net.URL; + +/** + * Class {@code PluginsCompanion} handles the local installation of + * JavaScript plugins. + * + * @author Hervé Bitteur + */ +public class PluginsCompanion + extends AbstractCompanion +{ + //~ Static fields/initializers --------------------------------------------- + + /** Usual logger utility */ + private static final Logger logger = LoggerFactory.getLogger( + PluginsCompanion.class); + + /** Tooltip. */ + private static final String DESC = "[This component is optional]" + + "
It installs JavaScript plugins" + + "
which can be customized manually."; + + /** Environment descriptor. */ + private static final Descriptor descriptor = DescriptorFactory.getDescriptor(); + + //~ Constructors ----------------------------------------------------------- + //------------------// + // PluginsCompanion // + //------------------// + /** + * Creates a new PluginsCompanion object. + */ + public PluginsCompanion () + { + super("Plugins", DESC); + need = Need.SELECTED; + + if (Installer.hasUI()) { + view = new BasicCompanionView(this, 90); + } + } + + //~ Methods ---------------------------------------------------------------- + //-----------// + // doInstall // + //-----------// + @Override + protected void doInstall () + throws Exception + { + final URI codeBase = Jnlp.basicService.getCodeBase() + .toURI(); + + final URL url = Utilities.toURI(codeBase, "resources/plugins.jar") + .toURL(); + + Utilities.downloadJarAndExpand( + "Plugins", + url.toString(), + descriptor.getTempFolder(), + "plugins", + makeTargetFolder()); + } + + //-----------------// + // getTargetFolder // + //-----------------// + @Override + protected File getTargetFolder () + { + return new File(descriptor.getConfigFolder(), "plugins"); + } +} diff --git a/src/installer/com/audiveris/installer/RegexUtil.java b/src/installer/com/audiveris/installer/RegexUtil.java new file mode 100644 index 0000000..02965d7 --- /dev/null +++ b/src/installer/com/audiveris/installer/RegexUtil.java @@ -0,0 +1,79 @@ +//----------------------------------------------------------------------------// +// // +// R e g e x U t i l // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// + +package com.audiveris.installer; + +import java.util.regex.Matcher; + +/** + * Class {@code RegexUtil} gathers utility features related to Regex. + * + * @author Hervé Bitteur + */ +public class RegexUtil +{ + //~ Methods ---------------------------------------------------------------- + + //----------// + // getGroup // + //----------// + /** + * Report the input sequence captured by the provided + * named-capturing group. + * + * @param matcher the matcher + * @param name the provided name for desired group + * @return the input sequence, perhaps empty but not null + */ + public static String getGroup (Matcher matcher, + String name) + { + String result = null; + + try { + result = matcher.group(name); + } catch (Exception ignored) { + } + + if (result != null) { + return result; + } else { + return ""; + } + } + + //-------// + // group // + //-------// + /** + * Convenient method to build a named-group like "(?content)" + * + * @param name name for the group + * @param content inner content of the group + * @return the ready to use group value + */ + public static String group (String name, + String content) + { + StringBuilder sb = new StringBuilder(); + + sb.append("(?<"); + sb.append(name); + sb.append(">"); + + sb.append(content); + + sb.append(")"); + + return sb.toString(); + } +} diff --git a/src/installer/com/audiveris/installer/SpecificFile.java b/src/installer/com/audiveris/installer/SpecificFile.java new file mode 100644 index 0000000..d6256bf --- /dev/null +++ b/src/installer/com/audiveris/installer/SpecificFile.java @@ -0,0 +1,53 @@ +//----------------------------------------------------------------------------// +// // +// S p e c i f i c F i l e // +// // +//----------------------------------------------------------------------------// +// +// Copyright © Herve 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. +//----------------------------------------------------------------------------// +// +package com.audiveris.installer; + +/** + * Class {@code SpecificFile} is used to govern the installation of + * a specific file in proper target location. + * All source files are provided in a common .jar file ("specifics.jar" as + * found in installer /resources folder). + * The purpose of this class is to define the source folder and name (in the + * common .jar file) and the target folder and name (in the user machine). + * + * @author Hervé Bitteur + */ +public class SpecificFile +{ + //~ Instance fields -------------------------------------------------------- + + /** Full resource name in source archive. */ + public final String source; + + /** Full target file path on user machine. */ + public final String target; + + /** Target to be set as executable. */ + public final boolean isExec; + + //~ Constructors ----------------------------------------------------------- + /** + * Creates a new SpecificFile object. + * + * @param source full resource name in source archive + * @param target full target path on user machine + * @param isExec true if target is to be set as executable + */ + public SpecificFile (String source, + String target, + boolean isExec) + { + this.source = source; + this.target = target; + this.isExec = isExec; + } +} diff --git a/src/installer/com/audiveris/installer/TrainingCompanion.java b/src/installer/com/audiveris/installer/TrainingCompanion.java new file mode 100644 index 0000000..81dc18e --- /dev/null +++ b/src/installer/com/audiveris/installer/TrainingCompanion.java @@ -0,0 +1,91 @@ +//----------------------------------------------------------------------------// +// // +// T r a i n i n g C o m p a n i o n // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package com.audiveris.installer; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.net.URI; +import java.net.URL; + +/** + * Class {@code TrainingCompanion} handles the installation of glyph + * samples used for (re)training Audiveris classifier. + * + * @author Hervé Bitteur + */ +public class TrainingCompanion + extends AbstractCompanion +{ + //~ Static fields/initializers --------------------------------------------- + + /** Usual logger utility */ + private static final Logger logger = LoggerFactory.getLogger( + TrainingCompanion.class); + + /** Tooltip. */ + private static final String DESC = "[This component is optional]" + + "
It installs samples that allow" + + "
to retrain the shape classifier."; + + /** Environment descriptor. */ + private static final Descriptor descriptor = DescriptorFactory.getDescriptor(); + + //~ Constructors ----------------------------------------------------------- + //-------------------// + // TrainingCompanion // + //-------------------// + /** + * Creates a new TrainingCompanion object. + */ + public TrainingCompanion () + { + super("Training", DESC); + need = Need.NOT_SELECTED; + + if (Installer.hasUI()) { + view = new BasicCompanionView(this, 95); + } + } + + //~ Methods ---------------------------------------------------------------- + //-----------// + // doInstall // + //-----------// + @Override + protected void doInstall () + throws Exception + { + final URI codeBase = Jnlp.basicService.getCodeBase() + .toURI(); + + final URL url = Utilities.toURI(codeBase, "resources/train.jar") + .toURL(); + + Utilities.downloadJarAndExpand( + "Training", + url.toString(), + descriptor.getTempFolder(), + "train", + makeTargetFolder()); + } + + //-----------------// + // getTargetFolder // + //-----------------// + @Override + protected File getTargetFolder () + { + return new File(descriptor.getDataFolder(), "train"); + } +} diff --git a/src/installer/com/audiveris/installer/TreeRemover.java b/src/installer/com/audiveris/installer/TreeRemover.java new file mode 100644 index 0000000..6081611 --- /dev/null +++ b/src/installer/com/audiveris/installer/TreeRemover.java @@ -0,0 +1,80 @@ +//----------------------------------------------------------------------------// +// // +// T r e e R e m o v e r // +// // +//----------------------------------------------------------------------------// +// +// Copyright © Herve 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. +//----------------------------------------------------------------------------// +// +package com.audiveris.installer; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.nio.file.FileVisitResult; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.SimpleFileVisitor; +import java.nio.file.attribute.BasicFileAttributes; + +/** + * Class {@code TreeRemover} is a FileVisitor that deletes a folder + * recursively. + * + * @author Hervé Bitteur + */ +public class TreeRemover + extends SimpleFileVisitor +{ + //~ Static fields/initializers --------------------------------------------- + + private static final Logger logger = LoggerFactory.getLogger( + TreeRemover.class); + + //~ Methods ---------------------------------------------------------------- + @Override + public FileVisitResult postVisitDirectory (Path dir, + IOException ex) + throws IOException + { + if (ex == null) { + logger.debug("Deleting directory {}", dir); + Files.delete(dir); + + return FileVisitResult.CONTINUE; + } else { + // directory iteration failed + throw ex; + } + } + + //--------// + // remove // + //--------// + /** + * Convenient method to remove a folder recursively. + * + * @param path the folder to remove, with all its content + * @throws IOException + */ + public static void remove (Path path) + throws IOException + { + Files.walkFileTree(path, new TreeRemover()); + } + + @Override + public FileVisitResult visitFile (Path file, + BasicFileAttributes attrs) + throws IOException + { + logger.debug("Deleting file {}", file); + Files.delete(file); + + return FileVisitResult.CONTINUE; + } +} diff --git a/src/installer/com/audiveris/installer/UnsupportedEnvironmentException.java b/src/installer/com/audiveris/installer/UnsupportedEnvironmentException.java new file mode 100644 index 0000000..f598ddf --- /dev/null +++ b/src/installer/com/audiveris/installer/UnsupportedEnvironmentException.java @@ -0,0 +1,64 @@ +//----------------------------------------------------------------------------// +// // +// U n s u p p o r t e d E n v i r o n m e n t E x c e p t i o n // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package com.audiveris.installer; + + +/** + * Class {@code UnsupportedEnvironmentException} is used to signal + * that the environment is not supported by the installer. + * + * @author Hervé Bitteur + */ +public class UnsupportedEnvironmentException + extends RuntimeException +{ + //~ Constructors ----------------------------------------------------------- + + /** + * Creates a new UnsupportedEnvironmentException object. + */ + public UnsupportedEnvironmentException () + { + } + + /** + * Creates a new UnsupportedEnvironmentException object. + * + * @param message DOCUMENT ME! + */ + public UnsupportedEnvironmentException (String message) + { + super(message); + } + + /** + * Creates a new UnsupportedEnvironmentException object. + * + * @param cause DOCUMENT ME! + */ + public UnsupportedEnvironmentException (Throwable cause) + { + super(cause); + } + + /** + * Creates a new UnsupportedEnvironmentException object. + * + * @param message DOCUMENT ME! + * @param cause DOCUMENT ME! + */ + public UnsupportedEnvironmentException (String message, + Throwable cause) + { + super(message, cause); + } +} diff --git a/src/installer/com/audiveris/installer/Utilities.java b/src/installer/com/audiveris/installer/Utilities.java new file mode 100644 index 0000000..5a5b694 --- /dev/null +++ b/src/installer/com/audiveris/installer/Utilities.java @@ -0,0 +1,322 @@ +//----------------------------------------------------------------------------// +// // +// U t i l i t i e s // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package com.audiveris.installer; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.BufferedReader; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.net.URLConnection; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.jar.JarFile; + +/** + * Class {@code Utilities} gathers helper methods for installation + * whatever the current environment. + * + * @author Hervé Bitteur + */ +public class Utilities +{ + //~ Static fields/initializers --------------------------------------------- + + /** Usual logger utility */ + private static final Logger logger = LoggerFactory.getLogger( + Utilities.class); + + //~ Methods ---------------------------------------------------------------- + //----------// + // download // + //----------// + /** + * Download data from a given url to the specified target file. + * + * @param urlString the url to download from + * @param target the target file + */ + public static void download (String urlString, + File target) + { + // Check url + URL url; + try { + url = new URL(urlString); + } catch (MalformedURLException ex) { + logger.warn("MalformedURLException", ex); + throw new RuntimeException(ex); + } + + // Check target + File parentDir = target.getParentFile(); + if (!parentDir.exists()) { + throw new RuntimeException("Target directory of " + target + + " does not exist"); + } + if (!parentDir.isDirectory()) { + throw new RuntimeException(parentDir + " is not a directory"); + } + + logger.info("Downloading {} to {} ...", url, target.getAbsolutePath()); + Jnlp.extensionInstallerService.setStatus("Downloading " + url); + + try { + URLConnection uc = url.openConnection(); + uc.setUseCaches(true); + + try (InputStream is = uc.getInputStream()) { + Files.copy(is, target.toPath(), StandardCopyOption.REPLACE_EXISTING); + } + logger.debug("End of download."); + Jnlp.extensionInstallerService.setStatus(""); + try { + Thread.sleep(500); + } catch (InterruptedException ex) { + logger.warn("Error on Thread.sleep", ex); + } + } catch (IOException ex) { + throw new RuntimeException("Error downloading " + urlString + + " to " + target.getAbsolutePath(), ex); + } + } + + //------------------------// + // downloadExecAndInstall // + //------------------------// + /** + * Download an executable from provided url and launch the + * executable with the installOption. + * + * @param title A title for the software to process + * @param url url to download from + * @param temp temp directory to be used for download + * @param installOption installation option if any, perhaps empty but not + * null + * @throws Throwable + */ + public static void downloadExecAndInstall (String title, + String url, + File temp, + String installOption) + throws Exception + { + // Download + final String fileName = new File(url).getName(); + final File exec = new File(temp, fileName); + download(url, exec); + + // Install + final List output = new ArrayList<>(); + try { + final int res = Utilities.runProcess( + output, exec.getAbsolutePath(), installOption); + if (res != 0) { + if (Installer.isAdmin) { + final String lines = Utilities.dumpOfLines(output); + logger.warn(lines); + throw new RuntimeException("Could not run " + exec + + ", exit: " + res + "\n" + lines); + } else { + postExec(exec, installOption); + } + } + } catch (IOException | InterruptedException | RuntimeException ex) { + if (Installer.isAdmin) { + logger.warn("Could not install " + title, ex); + throw ex; + } else { + postExec(exec, installOption); + } + } + } + + //----------// + // postExec // + //----------// + /** + * Post the launching of a program + * + * @param exec path to the executable + * @param options options, if any + */ + private static void postExec (File exec, + String... options) + { + StringBuilder sb = new StringBuilder(); + sb.append("\"").append(exec.getAbsolutePath()).append("\""); + for (String option : options) { + sb.append(" ").append(option); + } + + Installer.getBundle().appendCommand(sb.toString()); + } + + //----------------------// + // downloadJarAndExpand // + //----------------------// + /** + * Download a .jar file from a provided url and expand it to the + * desired target directory. + * + * @param title A title for the item to process + * @param url url to download from + * @param temp temp directory to be used for download + * @param root root filter + * @param targetDir target directory + * @throws Throwable + */ + public static void downloadJarAndExpand (String title, + String url, + File temp, + String root, + File targetDir) + throws Exception + { + // Download the .jar file + final String fileName = new File(url).getName(); + final File jarFile = new File(temp, fileName); + download(url, jarFile); + JarFile jar = new JarFile(jarFile); + + // Expand the jar file + if (!targetDir.exists()) { + if (targetDir.mkdirs()) { + logger.info("Created folder {}", targetDir.getAbsolutePath()); + } + } + JarExpander exp = new JarExpander(jar, root, targetDir); + exp.install(); + } + + //------------// + // runProcess // + //------------// + /** + * Launch a process. + * + * @param output (output) the output lines (stdout and stderr) + * @param args (input) executable and arguments + * @return the process exit code + * @throws IOException + * @throws InterruptedException + */ + public static int runProcess (List output, + String... args) + throws IOException, InterruptedException + { + // Command arguments + List cmdArgs = Arrays.asList(args); + logger.debug("runProcess args: {}", cmdArgs); + + try { + // Spawn process + ProcessBuilder pb = new ProcessBuilder(cmdArgs); + pb.redirectErrorStream(true); + + Process process = pb.start(); + + // Read output + InputStream is = process.getInputStream(); + InputStreamReader isr = new InputStreamReader(is); + BufferedReader br = new BufferedReader(isr); + String line; + + while ((line = br.readLine()) != null) { + logger.debug("line: {}", line); + output.add(line); + } + + // Wait for process completion + int exitValue = process.waitFor(); + logger.debug("Exit value is: {}", exitValue); + + return exitValue; + } catch (Exception ex) { + logger.warn("[isAdmin: " + Installer.isAdmin + + "] Error running " + cmdArgs, ex); + throw ex; + } + } + + //-------// + // toURI // + //-------// + /** + * Convenient method to simulate a parent/child composition + * + * @param parent the URI to parent directory + * @param child the child name + * @return the resulting URI + */ + public static URI toURI (URI parent, + String child) + { + try { + // Make sure parent ends with a '/' + if (parent == null) { + throw new IllegalArgumentException("Parent is null"); + } + + StringBuilder dirName = new StringBuilder(parent.toString()); + + if (dirName.charAt(dirName.length() - 1) != '/') { + dirName.append('/'); + } + + // Make sure child does not start with a '/' + if ((child == null) || child.isEmpty()) { + throw new IllegalArgumentException("Child is null or empty"); + } + + if (child.startsWith("/")) { + throw new IllegalArgumentException( + "Child is absolute: " + child); + } + + return new URI(dirName.append(child).toString()); + } catch (URISyntaxException ex) { + throw new IllegalArgumentException(ex.getMessage(), ex); + } + } + + //-------------// + // dumpOfLines // + //-------------// + /** + * Meant for dumping a bunch of lines + * + * @param lines the lines to dump + * @return one string for all lines + */ + public static String dumpOfLines (List lines) + { + StringBuilder sb = new StringBuilder(); + + for (String line : lines) { + sb.append("\n>>> ").append(line); + } + + return sb.toString(); + } +} diff --git a/src/installer/com/audiveris/installer/ViewAppender.java b/src/installer/com/audiveris/installer/ViewAppender.java new file mode 100644 index 0000000..3f68b5a --- /dev/null +++ b/src/installer/com/audiveris/installer/ViewAppender.java @@ -0,0 +1,84 @@ +//----------------------------------------------------------------------------// +// // +// V i e w A p p e n d e r // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package com.audiveris.installer; + +import ch.qos.logback.classic.Level; +import ch.qos.logback.classic.spi.ILoggingEvent; +import ch.qos.logback.core.AppenderBase; + +import java.util.ArrayList; +import java.util.List; + +/** + * Class {@code ViewAppender} is a log appender that appends the + * logging messages to the BundleView. + * + * @author Hervé Bitteur + */ +public class ViewAppender + extends AppenderBase +{ + //~ Instance fields -------------------------------------------------------- + + /** The installer bundle view. */ + private BundleView view; + + /** Needed to store messages until the view gets available. */ + private final List backlog = new ArrayList(); + + //~ Methods ---------------------------------------------------------------- + + //--------// + // append // + //--------// + @Override + protected void append (ILoggingEvent event) + { + if (!isReady()) { + backlog.add(event); + } else { + if (!backlog.isEmpty()) { + for (ILoggingEvent evt : backlog) { + publish(evt); + } + + backlog.clear(); + } + + publish(event); + } + } + + //---------// + // isReady // + //---------// + private boolean isReady () + { + if (Installer.getBundle() == null) { + return false; + } + + view = Installer.getBundle() + .getView(); + + return view != null; + } + + //---------// + // publish // + //---------// + private void publish (ILoggingEvent event) + { + Level level = event.getLevel(); + view.publishMessage(level, event.getFormattedMessage()); + } +} diff --git a/src/installer/com/audiveris/installer/mac/package.html b/src/installer/com/audiveris/installer/mac/package.html new file mode 100644 index 0000000..2e50158 --- /dev/null +++ b/src/installer/com/audiveris/installer/mac/package.html @@ -0,0 +1,14 @@ + + + + + Package com.audiveris.installer.mac + + + +

+ Specific implementations for Mac environment. +

+ + + diff --git a/src/installer/com/audiveris/installer/package.html b/src/installer/com/audiveris/installer/package.html new file mode 100644 index 0000000..93ae0ff --- /dev/null +++ b/src/installer/com/audiveris/installer/package.html @@ -0,0 +1,13 @@ + + + + + Package com.audiveris.installer + + + +

+ Core installation features, regardless of target OS. +

+ + diff --git a/src/installer/com/audiveris/installer/unix/Package.java b/src/installer/com/audiveris/installer/unix/Package.java new file mode 100644 index 0000000..d8c5fc9 --- /dev/null +++ b/src/installer/com/audiveris/installer/unix/Package.java @@ -0,0 +1,186 @@ +//----------------------------------------------------------------------------// +// // +// P a c k a g e // +// // +//----------------------------------------------------------------------------// +// // +// Copyright © Herve 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. // +//----------------------------------------------------------------------------// +// +package com.audiveris.installer.unix; + +import com.audiveris.installer.Installer; +import com.audiveris.installer.RegexUtil; +import com.audiveris.installer.Utilities; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Class {@code Package} handles a Linux package requirement and + * installation, using the underlying "apt" and dpkg" utilities. + * + * @author Hervé Bitteur + */ +public class Package +{ + //~ Static fields/initializers --------------------------------------------- + + /** + * Usual logger utility + */ + private static final Logger logger = LoggerFactory.getLogger(Package.class); + + //~ Instance fields -------------------------------------------------------- + /** + * Name of the package. + */ + public final String name; + + /** + * Minimum version required. + */ + public final VersionNumber minVersion; + + //~ Constructors ----------------------------------------------------------- + //---------// + // Package // + //---------// + /** + * Create a Package instance. + * + * @param name the package name + * @param minVersion the minimum required version + */ + public Package (String name, + String minVersion) + { + if ((name == null) || name.isEmpty()) { + throw new IllegalArgumentException("Null or empty package name"); + } + + if ((minVersion == null) || minVersion.isEmpty()) { + throw new IllegalArgumentException( + "Null or empty package minimum version"); + } + + this.name = name; + this.minVersion = new VersionNumber(minVersion); + } + + //~ Methods ---------------------------------------------------------------- + //---------------------// + // getInstalledVersion // + //---------------------// + /** + * Report the version number of this package, if installed. + * + * @return the version number, or null if not found + */ + public VersionNumber getInstalledVersion () + { + final String VERSION = "version"; + final Pattern versionPattern = Pattern.compile( + "^Version: " + RegexUtil.group(VERSION, ".*") + "$"); + + final String STATUS = "status"; + final Pattern statusPattern = Pattern.compile( + "^Status: " + RegexUtil.group(STATUS, ".*") + "$"); + + try { + List output = new ArrayList(); + int res = Utilities.runProcess( + output, + "bash", + "-c", + "LANG=en_US dpkg -s " + name); + + if (res != 0) { + // If we get a non-zero exit code no info can be retrieved + if (!output.isEmpty()) { + final String lines = Utilities.dumpOfLines(output); + logger.info(lines); + } else { + logger.debug("No command output"); + } + + return null; + } else { + // Check status + boolean installed = false; + + for (String line : output) { + Matcher matcher = statusPattern.matcher(line); + + if (matcher.matches()) { + String statusStr = RegexUtil.getGroup(matcher, STATUS); + + if (statusStr.equals("install ok installed")) { + installed = true; + + break; + } + } + } + + if (!installed) { + return null; + } + + // Return installed version + for (String line : output) { + Matcher matcher = versionPattern.matcher(line); + + if (matcher.matches()) { + String versionStr = RegexUtil.getGroup( + matcher, + VERSION); + + return new VersionNumber(versionStr); + } + } + } + } catch (Throwable ex) { + logger.warn("Could not get package version", ex); + } + + return null; + } + + //---------// + // install // + //---------// + /** + * Install the latest version of this package + * + * @throws Exception + */ + public void install () + throws Exception + { + String cmd = "apt-get install -y " + name; + Installer.getBundle() + .appendCommand(cmd); + } + + //-------------// + // isInstalled // + //-------------// + public boolean isInstalled () + { + VersionNumber installedVersion = getInstalledVersion(); + + if (installedVersion == null) { + return false; + } + + return installedVersion.compareTo(minVersion) >= 0; + } +} diff --git a/src/installer/com/audiveris/installer/unix/UnixDescriptor.java b/src/installer/com/audiveris/installer/unix/UnixDescriptor.java new file mode 100644 index 0000000..84fa752 --- /dev/null +++ b/src/installer/com/audiveris/installer/unix/UnixDescriptor.java @@ -0,0 +1,424 @@ +//----------------------------------------------------------------------------// +// // +// U n i x D e s c r i p t o r // +// // +//----------------------------------------------------------------------------// +// // +// Copyright © Herve 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. // +//----------------------------------------------------------------------------// +// +package com.audiveris.installer.unix; + +import com.audiveris.installer.Descriptor; +import com.audiveris.installer.SpecificFile; +import com.audiveris.installer.Utilities; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * Class {@code UnixDescriptor} implements Installer descriptor for + * Linux Ubuntu (32 and 64 bits) + * + * @author Hervé Bitteur + */ +public class UnixDescriptor + implements Descriptor +{ + //~ Static fields/initializers --------------------------------------------- + + private static final Logger logger = LoggerFactory.getLogger( + UnixDescriptor.class); + + /** + * Specific prefix for application folders. {@value} + */ + private static final String TOOL_PREFIX = "/" + COMPANY_ID + "/" + + TOOL_NAME; + + /** + * Set of requirements for c/c++. + */ + private static final Package[] cReqs = new Package[]{ + new Package("libc6", "2.15"), + new Package("libgcc1", "4.6.3"), + new Package( + "libstdc++6", + "4.6.3") + }; + + /** + * Requirement for ghostscript. + */ + private static final Package gsReq = new Package( + "ghostscript", + "9.06~dfsg"); + + /** + * Requirement for tesseract. + */ + private static final Package tessReq = new Package("libtesseract3", "3.02"); + + /** + * KDE front-end for sudo. + */ + private static final String KDESUDO = "kdesudo"; + + /** + * GTK+ front-end for sudo. + */ + private static final String GKSUDO = "gksudo"; + + //~ Methods ---------------------------------------------------------------- + //-----------------// + // getConfigFolder // + //-----------------// + @Override + public File getConfigFolder () + { + String config = System.getenv("XDG_CONFIG_HOME"); + + if (config != null) { + return new File(config + TOOL_PREFIX); + } + + String home = System.getenv("HOME"); + + if (home != null) { + return new File(home + "/.config" + TOOL_PREFIX); + } + + throw new RuntimeException("HOME environment variable is not set"); + } + + //----------------// + // getCopyCommand // + //----------------// + @Override + public String getCopyCommand (Path source, + Path target) + { + return "cp -r -v \"" + source.toAbsolutePath() + "\" \"" + + target.toAbsolutePath() + "\""; + } + + //---------------// + // getDataFolder // + //---------------// + @Override + public File getDataFolder () + { + String data = System.getenv("XDG_DATA_HOME"); + + if (data != null) { + return new File(data + TOOL_PREFIX); + } + + String home = System.getenv("HOME"); + + if (home != null) { + return new File(home + "/.local/share" + TOOL_PREFIX); + } + + throw new RuntimeException("HOME environment variable is not set"); + } + + //--------------------------// + // getDefaultTessdataPrefix // + //--------------------------// + @Override + public File getDefaultTessdataPrefix () + { + return new File("/usr/share/" + Descriptor.TESSERACT_OCR + "/"); + } + + //------------------// + // getDeleteCommand // + //------------------// + @Override + public String getDeleteCommand (Path file) + { + return "rm -v -f \"" + file.toAbsolutePath() + "\""; + } + + //-----------------// + // getMkdirCommand // + //-----------------// + @Override + public String getMkdirCommand (Path dir) + { + return "mkdir -v -p \"" + dir.toAbsolutePath() + "\""; + } + + //-------------------// + // getSetExecCommand // + //-------------------// + @Override + public String getSetExecCommand (Path file) + { + return "chmod -v a+x \"" + file.toAbsolutePath() + "\""; + } + + //------------------// + // getSpecificFiles // + //------------------// + @Override + public List getSpecificFiles () + { + return Arrays.asList( + new SpecificFile("unix/audiveris.sh", "/usr/bin/audiveris", true), + new SpecificFile( + "unix/AddPlugins.sh", + "/usr/share/audiveris/AddPlugins.sh", + true)); + } + + //---------------// + // getTempFolder // + //---------------// + @Override + public File getTempFolder () + { + final File folder = new File(getDataFolder(), "temp/installation"); + logger.debug("getTempFolder: {}", folder.getAbsolutePath()); + + return folder; + } + + //------------// + // installCpp // + //------------// + @Override + public void installCpp () + throws Exception + { + for (Package pkg : cReqs) { + if (!pkg.isInstalled()) { + pkg.install(); + } + } + } + + //--------------------// + // installGhostscript // + //--------------------// + @Override + public void installGhostscript () + throws Exception + { + gsReq.install(); + } + + //------------------// + // installTesseract // + //------------------// + @Override + public void installTesseract () + throws Exception + { + tessReq.install(); + } + + //---------// + // isAdmin // + //---------// + @Override + public boolean isAdmin () + { + // whoami -> herve + // sudo whoami -> root + try { + List output = new ArrayList(); + int res = Utilities.runProcess( + output, + "bash", + "-c", + "whoami"); + + if (res != 0) { + final String lines = Utilities.dumpOfLines(output); + logger.warn(lines); + throw new RuntimeException( + "Failure in isAdmin(). exit: " + res + "\n" + lines); + } else { + return !output.isEmpty() && output.get(0) + .equals("root"); + } + } catch (Exception ex) { + logger.warn("Failure in isAdmin(). ", ex); + + return true; // Safer + } + } + + //----------------// + // isCppInstalled // + //----------------// + @Override + public boolean isCppInstalled () + { + for (Package pkg : cReqs) { + if (!pkg.isInstalled()) { + return false; + } + } + + return true; + } + + //------------------------// + // isGhostscriptInstalled // + //------------------------// + @Override + public boolean isGhostscriptInstalled () + { + return gsReq.isInstalled(); + } + + //----------------------// + // isTesseractInstalled // + //----------------------// + @Override + public boolean isTesseractInstalled () + { + return tessReq.isInstalled(); + } + + //----------// + // runShell // + //----------// + @Override + public boolean runShell (boolean asAdmin, + List commands) + throws Exception + { + // Build a single compound command + StringBuilder sb = new StringBuilder(); + + for (String command : commands) { + if (sb.length() > 0) { + sb.append(" ; "); + } + + sb.append(command); + } + + String cmdLine = sb.toString(); + + // If we have to run as Admin, pick up proper sudo front-end + String sudoFrontend = ""; + + if (asAdmin) { + for (String name : new String[]{GKSUDO, KDESUDO}) { + if (isKnown(name)) { + sudoFrontend = name; + logger.info("\nUsing {}", sudoFrontend); + break; + } + } + } + + List output = new ArrayList(); + + try { + final int res; + switch (sudoFrontend) { + case GKSUDO: + res = Utilities.runProcess( + output, + GKSUDO, + "-DAudiveris installer", + "bash -v -e -c '" + cmdLine + "'"); + break; + + case KDESUDO: + res = Utilities.runProcess( + output, + KDESUDO, + "bash -v -e -c '" + cmdLine + "'"); + break; + + default: + res = Utilities.runProcess( + output, + "bash", + "-v", + "-e", + "-c", + cmdLine); + } + + if (res == 0) { + return true; // Normal exit + } else { + output.add("Exit code = " + res); + } + } catch (Exception ex) { + logger.warn("Exception in runProcess", ex); + } + + // If we are getting here, we failed! + final String lines = Utilities.dumpOfLines(output); + + logger.warn(lines); + + throw new RuntimeException( + "Failure in runShell().\n" + lines); + } + + //---------// + // isKnown // + //---------// + /** + * Check whether the provided command name is known + * + * @param name the command name + * @return true if known, false otherwise + */ + private boolean isKnown (String name) + { + List output = new ArrayList(); + try { + int res = Utilities.runProcess( + output, + "bash", + "-c", + "which " + name); + logger.debug(Utilities.dumpOfLines(output)); + return res == 0; + } catch (Exception ex) { + final String lines = Utilities.dumpOfLines(output); + logger.warn("Failure in isKnown(). ex: " + ex + "\n" + lines); + return false; + } + } + + //---------------// + // setExecutable // + //---------------// + @Override + public void setExecutable (Path file) + throws Exception + { + List output = new ArrayList(); + int res = Utilities.runProcess( + output, + "bash", + "-c", + "chmod -v a+x \"" + file.toAbsolutePath() + "\""); + + if (res != 0) { + final String lines = Utilities.dumpOfLines(output); + logger.warn(lines); + throw new RuntimeException("Failure in setExecutable().\n" + lines); + } + } +} diff --git a/src/installer/com/audiveris/installer/unix/UnixUtilities.java b/src/installer/com/audiveris/installer/unix/UnixUtilities.java new file mode 100644 index 0000000..34b83f5 --- /dev/null +++ b/src/installer/com/audiveris/installer/unix/UnixUtilities.java @@ -0,0 +1,92 @@ +//----------------------------------------------------------------------------// +// // +// U n i x U t i l i t i e s // +// // +//----------------------------------------------------------------------------// +// // +// Copyright © Herve 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. // +//----------------------------------------------------------------------------// +// +package com.audiveris.installer.unix; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.List; + +/** + * Class {@code UnixUtilities} gathers basic utilities for Unix. + * + * @author Hervé Bitteur + */ +public class UnixUtilities +{ + //~ Static fields/initializers --------------------------------------------- + + /** Usual logger utility */ + private static final Logger logger = LoggerFactory.getLogger( + UnixUtilities.class); + + //~ Methods ---------------------------------------------------------------- + //----------------// + // getCommandLine // + //----------------// + /** + * Report the command line for the current process. + * + * @return the command line, or null if failed + */ + public static String getCommandLine () + { + try { + // Retrieve my command line + Path cmd = Paths.get("/proc/" + getPid() + "/cmdline"); + + if (Files.exists(cmd)) { + List lines = Files.readAllLines( + cmd, + StandardCharsets.US_ASCII); + + if (!lines.isEmpty()) { + // BEWARE: spaces between arguments have been converted to 0 + String cmdLine = lines.get(0) + .replace((char) 0, ' ') + .trim(); + logger.debug("cmdline: {}", cmdLine); + + return cmdLine; + } + } + } catch (IOException ex) { + logger.warn("Error in getCommandLine()", ex); + } + + return null; + } + + //--------// + // getPid // + //--------// + /** + * Report the pid of the current process. + * + * @return the process id, as a string + */ + public static String getPid () + throws IOException + { + String pid = new File("/proc/self").getCanonicalFile() + .getName(); + logger.debug("pid: {}", pid); + + return pid; + } +} diff --git a/src/installer/com/audiveris/installer/unix/VersionNumber.java b/src/installer/com/audiveris/installer/unix/VersionNumber.java new file mode 100644 index 0000000..f9bd96b --- /dev/null +++ b/src/installer/com/audiveris/installer/unix/VersionNumber.java @@ -0,0 +1,380 @@ +//----------------------------------------------------------------------------// +// // +// V e r s i o n N u m b e r // +// // +//----------------------------------------------------------------------------// +// // +// Copyright © Herve 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. // +//----------------------------------------------------------------------------// +// +package com.audiveris.installer.unix; + +import static com.audiveris.installer.RegexUtil.*; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Class {@code VersionNumber} handles the version number of a Debian + * package, and especially version comparison. + *

+ * Based on + * http://www.debian.org/doc/debian-policy/ch-controlfields.html#s-f-Version + *

+ * Pattern is [epoch:]upstream_version[-debian_revision] + * epoch : integer + * upstream_version : [A-Za-z0-9.+-:~] starts with a digit + * debian_revision : [A-Za-z0-9.+~] + * + * @author Hervé Bitteur + */ +public class VersionNumber + implements Comparable +{ + //~ Static fields/initializers --------------------------------------------- + + /** Usual logger utility */ + private static final Logger logger = LoggerFactory.getLogger( + VersionNumber.class); + + /** Regex for epoch. */ + private static final Pattern epochPattern = Pattern.compile("^[0-9]+$"); + + /** Regex for version. */ + private static final Pattern versionPattern = Pattern.compile( + "^[0-9][A-Za-z0-9\\.+-:~]*$"); + + /** Regex for revision. */ + private static final Pattern revisionPattern = Pattern.compile( + "^[A-Za-z0-9\\.+~]+$"); + + /** Regex for parsing version (and revision) string. */ + private static final String LETTERS = "letters"; + private static final String DIGITS = "digits"; + private static final String REM = "rem"; + private static final Pattern lettersPattern = Pattern.compile( + "^" + group(LETTERS, "[^0-9]*") + group(REM, ".*") + "$"); + private static final Pattern digitsPattern = Pattern.compile( + "^" + group(DIGITS, "[0-9]*") + group(REM, ".*") + "$"); + + //~ Instance fields -------------------------------------------------------- + + /** The original string (for debugging). */ + private final String source; + + /** The epoch part, if any. */ + private final int epoch; + + /** The upstream_version (mandatory). */ + private final String version; + + /** The debian_revision, if any. */ + private final String revision; + + //~ Constructors ----------------------------------------------------------- + + //---------------// + // VersionNumber // + //---------------// + /** + * Creates a new VersionNumber object. + * + * @param source the source string to be parsed + */ + public VersionNumber (String source) + { + this.source = source; + + String src = source.trim(); + + // Epoch is a single (generally small) unsigned integer. + // It may be omitted, in which case zero is assumed. + // If it is omitted then the upstream_version may not contain any colons. + int colon = src.indexOf(":"); + + if (colon != -1) { + String epochStr = src.substring(0, colon); + checkEpoch(epochStr); + epoch = Integer.parseInt(epochStr); + src = src.substring(colon + 1); + } else { + epoch = 0; + } + + // Break the version number apart at the last hyphen in the string + // (if there is one) to determine the upstream_version and debian_revision + int hyphen = src.lastIndexOf("-"); + + if (hyphen != -1) { + version = src.substring(0, hyphen); + checkVersion(version); + revision = src.substring(hyphen + 1); + checkRevision(revision); + } else { + version = src; + checkVersion(version); + revision = "0"; + } + + logger.debug("VersionNumber: {}", this); + } + + //~ Methods ---------------------------------------------------------------- + + //-----------// + // compareTo // + //-----------// + @Override + public int compareTo (VersionNumber that) + { + // 1/ epoch + int res = Integer.compare(this.epoch, that.epoch); + + if (res != 0) { + return res; + } + + // 2/ version + res = compareStrings(this.version, that.version); + + if (res != 0) { + return res; + } + + // 3/ revision + return compareStrings(this.revision, that.revision); + } + + //----------// + // toString // + //----------// + @Override + public String toString () + { + StringBuilder sb = new StringBuilder("{VersionNumber"); + + sb.append(" source=") + .append(source); + sb.append(" epoch=") + .append(epoch); + sb.append(" version=") + .append(version); + sb.append(" revision=") + .append(revision); + + sb.append("}"); + + return sb.toString(); + } + + //------------// + // checkEpoch // + //------------// + private void checkEpoch (String str) + { + Matcher matcher = epochPattern.matcher(str); + + if (!matcher.matches()) { + throw new IllegalArgumentException("Illegal epoch string " + str); + } + } + + //--------------// + // checkVersion // + //--------------// + private void checkRevision (String str) + { + Matcher matcher = revisionPattern.matcher(str); + + if (!matcher.matches()) { + throw new IllegalArgumentException( + "Illegal revision string " + str); + } + } + + //--------------// + // checkVersion // + //--------------// + private void checkVersion (String str) + { + Matcher matcher = versionPattern.matcher(str); + + if (!matcher.matches()) { + throw new IllegalArgumentException("Illegal version string " + str); + } + } + + //---------------// + // compareDigits // + //---------------// + private int compareDigits (String oneStr, + String twoStr) + { + /* + * The numerical values of + * these two parts are compared, and any difference found is returned as + * the result of the comparison. For these purposes an empty string + * (which can only occur at the end of one or both version strings + * being compared) counts as zero. + */ + int one = oneStr.isEmpty() ? 0 : Integer.parseInt(oneStr); + int two = twoStr.isEmpty() ? 0 : Integer.parseInt(twoStr); + + return Integer.compare(one, two); + } + + //----------------// + // compareLetters // + //----------------// + private int compareLetters (String oneStr, + String twoStr) + { + /* + * The lexical comparison is a comparison of ASCII values + * modified so that all the letters sort earlier than all the + * non-letters and so that a tilde sorts before anything, even the end + * of a part. For example, the following parts are in sorted order from + * earliest to latest: ~~, ~~a, ~, the empty part, a. + */ + final int max = Math.max(oneStr.length(), twoStr.length()); + int res; + + for (int i = 0; i < max; i++) { + // We code empty parts with spaces (which are not allowed in source) + char one = (i < oneStr.length()) ? oneStr.charAt(i) : ' '; + char two = (i < twoStr.length()) ? twoStr.charAt(i) : ' '; + + // Tilde über alles (even empty part, which is coded as space) + if (one == '~') { + if (two != '~') { + return -1; + } + } else { + if (two == '~') { + return 1; + } + } + + // Empty (space) before everything (except tilde) + if (one == ' ') { + return -1; + } + + if (two == ' ') { + return 1; + } + + // Letters before non-letters + if (Character.isLetter(one)) { + if (Character.isLetter(two)) { + res = one - two; + + if (res != 0) { + return res; + } + } else { + return -1; + } + } else { + if (Character.isLetter(two)) { + return 1; + } else { + res = one - two; + + if (res != 0) { + return res; + } + } + } + } + + return 0; + } + + //----------------// + // compareStrings // + //----------------// + private int compareStrings (String one, + String two) + { + while (true) { + /* + * First the initial part of each string consisting entirely of + * non-digit characters is determined. These two parts (one of which + * may be empty) are compared lexically. If a difference is found it is + * returned. + */ + final String oneLetters; + Matcher oneLettersMatcher = lettersPattern.matcher(one); + + if (oneLettersMatcher.matches()) { + oneLetters = getGroup(oneLettersMatcher, LETTERS); + one = getGroup(oneLettersMatcher, REM); + } else { + oneLetters = ""; + } + + final String twoLetters; + Matcher twoLettersMatcher = lettersPattern.matcher(two); + + if (twoLettersMatcher.matches()) { + twoLetters = getGroup(twoLettersMatcher, LETTERS); + two = getGroup(twoLettersMatcher, REM); + } else { + twoLetters = ""; + } + + int res = compareLetters(oneLetters, twoLetters); + + if (res != 0) { + return res; + } + + /* + * Then the initial part of the remainder of each string which consists + * entirely of digit characters is determined. The numerical values of + * these two parts are compared, and any difference found is returned as + * the result of the comparison. + */ + final String oneDigits; + Matcher oneDigitsMatcher = digitsPattern.matcher(one); + + if (oneDigitsMatcher.matches()) { + oneDigits = getGroup(oneDigitsMatcher, DIGITS); + one = getGroup(oneDigitsMatcher, REM); + } else { + oneDigits = ""; + } + + final String twoDigits; + Matcher twoDigitsMatcher = digitsPattern.matcher(two); + + if (twoDigitsMatcher.matches()) { + twoDigits = getGroup(twoDigitsMatcher, DIGITS); + two = getGroup(twoDigitsMatcher, REM); + } else { + twoDigits = ""; + } + + res = compareDigits(oneDigits, twoDigits); + + if (res != 0) { + return res; + } + + /* + * These two steps (comparing and removing initial non-digit strings and + * initial digit strings) are repeated until a difference is found or + * both strings are exhausted. + */ + if (one.isEmpty() && two.isEmpty()) { + return 0; + } + } + } +} diff --git a/src/installer/com/audiveris/installer/unix/package.html b/src/installer/com/audiveris/installer/unix/package.html new file mode 100644 index 0000000..f450b84 --- /dev/null +++ b/src/installer/com/audiveris/installer/unix/package.html @@ -0,0 +1,14 @@ + + + + + Package com.audiveris.installer.unix + + + +

+ Specific implementations for Unix environment. +

+ + + diff --git a/src/installer/com/audiveris/installer/windows/WindowsDescriptor.java b/src/installer/com/audiveris/installer/windows/WindowsDescriptor.java new file mode 100644 index 0000000..19349f6 --- /dev/null +++ b/src/installer/com/audiveris/installer/windows/WindowsDescriptor.java @@ -0,0 +1,737 @@ +//----------------------------------------------------------------------------// +// // +// W i n d o w s D e s c r i p t o r // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package com.audiveris.installer.windows; + +import com.audiveris.installer.Descriptor; +import com.audiveris.installer.DescriptorFactory; +import com.audiveris.installer.Installer; +import com.audiveris.installer.Jnlp; +import static com.audiveris.installer.OcrCompanion.LOCAL_LIB_FOLDER; +import static com.audiveris.installer.RegexUtil.*; +import com.audiveris.installer.SpecificFile; +import com.audiveris.installer.Utilities; + +import hudson.util.jna.Shell32; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.io.IOException; +import java.net.URI; +import java.net.URL; +import java.nio.file.FileVisitResult; +import static java.nio.file.FileVisitResult.CONTINUE; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.SimpleFileVisitor; +import static java.nio.file.StandardCopyOption.REPLACE_EXISTING; +import java.nio.file.attribute.BasicFileAttributes; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Class {@code WindowsDescriptor} implements Installer descriptor for + * Windows (32 and 64 bits). + * + * @author Hervé Bitteur + */ +public class WindowsDescriptor + implements Descriptor +{ + //~ Static fields/initializers --------------------------------------------- + + /** + * Usual logger utility + */ + private static final Logger logger = LoggerFactory.getLogger( + WindowsDescriptor.class); + + /** + * Specific prefix for application folders. {@value} + */ + private static final String TOOL_PREFIX = "/" + COMPANY_ID + "/" + + TOOL_NAME; + + //~ Methods ---------------------------------------------------------------- + // + //-----------------// + // getConfigFolder // + //-----------------// + @Override + public File getConfigFolder () + { + final String appdata = System.getenv("APPDATA"); + final File root = new File(appdata + TOOL_PREFIX); + final File file = new File(root, "config"); + logger.debug("getConfigFolder: {}", file.getAbsolutePath()); + + return file; + } + + //----------------// + // getCopyCommand // + //----------------// + @Override + public String getCopyCommand (Path source, + Path target) + { + return "XCOPY \"" + source.toAbsolutePath() + "\" \"" + + target.toAbsolutePath() + "\" /E /F /C /I /R /Y"; + } + + //---------------// + // getDataFolder // + //---------------// + @Override + public File getDataFolder () + { + final String appdata = System.getenv("APPDATA"); + final File root = new File(appdata + TOOL_PREFIX); + final File file = new File(root, "data"); + logger.debug("getDataFolder: {}", file.getAbsolutePath()); + + return file; + } + + //--------------------------// + // getDefaultTessdataPrefix // + //--------------------------// + @Override + public File getDefaultTessdataPrefix () + { + final String pf32 = DescriptorFactory.OS_ARCH.equals("x86") + ? "ProgramFiles" : "ProgramFiles(x86)"; + final String target = System.getenv(pf32); + final File file = new File( + new File(target), + Descriptor.TESSERACT_OCR); + logger.debug("getDefaultTessdataPrefix: {}", file.getAbsolutePath()); + + return file; + } + + //------------------// + // getDeleteCommand // + //------------------// + @Override + public String getDeleteCommand (Path file) + { + return "DEL /F \"" + file.toAbsolutePath() + "\""; + } + + //-----------------// + // getMkdirCommand // + //-----------------// + @Override + public String getMkdirCommand (Path dir) + { + return "mkdir \"" + dir.toAbsolutePath() + "\""; + } + + //-------------------// + // getSetExecCommand // + //-------------------// + @Override + public String getSetExecCommand (Path file) + { + // This is void on Windows + return ""; + } + + //------------------// + // getSpecificFiles // + //------------------// + @Override + public List getSpecificFiles () + { + final String appdata = System.getenv("APPDATA"); + final File root = new File(appdata + TOOL_PREFIX); + + return Arrays.asList( + new SpecificFile( + "windows/audiveris.bat", + root.getAbsolutePath() + "/audiveris.bat", + false)); + } + + //---------------// + // getTempFolder // + //---------------// + @Override + public File getTempFolder () + { + final File folder = new File(getDataFolder(), "temp/installation"); + logger.debug("getTempFolder: {}", folder.getAbsolutePath()); + + return folder; + } + + //------------// + // installCpp // + //------------// + @Override + public void installCpp () + throws Exception + { + final String url = DescriptorFactory.OS_ARCH.equals("x86") ? CPP.URL_32 + : CPP.URL_64; + Utilities.downloadExecAndInstall( + "C++ runtime", + url, + getTempFolder(), + "/q"); + } + + //--------------------// + // installGhostscript // + //--------------------// + @Override + public void installGhostscript () + throws Exception + { + final String url = DescriptorFactory.OS_ARCH.equals("x86") ? GS.URL_32 + : GS.URL_64; + Utilities.downloadExecAndInstall( + "Ghostscript", + url, + getTempFolder(), + "/S"); + } + + //------------------// + // installTesseract // + //------------------// + @Override + public void installTesseract () + throws Exception + { + /** + * The current strategy (for Windows) is to copy all needed + * DLLs into the proper target system folder. + * (The previous strategy was to keep them in Java cache, but we faced + * problems when trying to load them explicitly). + * As usual, we download and expand locally, then try to copy the + * expanded files to target system folder. + * If direct copy is denied, we post copy commands to be run later at + * elevated level. + */ + final File local = getLocalLibFolder(); + final File lib = new File(local, "lib"); + lib.mkdirs(); + logger.debug("local lib folder: {}", lib.getAbsolutePath()); + + final URI codeBase = Jnlp.basicService.getCodeBase() + .toURI(); + final String jarName = DescriptorFactory.OS_ARCH.equals("x86") + ? TESS.JAR_32 : TESS.JAR_64; + final URL url = Utilities.toURI(codeBase, jarName) + .toURL(); + Utilities.downloadJarAndExpand( + "Tesseract", + url.toString(), + getTempFolder(), + "", + lib); + + // (Try to) copy each file from local lib folder to system target folder + final Path libPath = lib.toPath(); + final String sysDir = DescriptorFactory.WOW ? TESS.SYSTEM_WOW + : TESS.SYSTEM_PURE; + final Path target = Paths.get(sysDir); + + Files.walkFileTree( + libPath, + new SimpleFileVisitor() + { + @Override + public FileVisitResult visitFile (Path file, + BasicFileAttributes attrs) + throws IOException + { + Path dest = target.resolve(file.getFileName()); + + try { + // Try immediate copy + Files.copy(file, dest, REPLACE_EXISTING); + logger.info( + "Copied file {} to file {}", + file, + dest); + } catch (IOException ex) { + // Fallback to posted copy commands + Installer.getBundle() + .appendCommand(getCopyCommand(file, target)); + } + + return CONTINUE; + } + }); + } + + //---------// + // isAdmin // + //---------// + @Override + public boolean isAdmin () + { + // The UAC (User Access Control) appeared with Windows Vista + // Before that, user was granted admin privileges by default + try { + // If the IsUserAnAdmin method exists, then we are in Vista or later + // and just need to check its result + return Shell32.INSTANCE.IsUserAnAdmin(); + } catch (Throwable ex) { + // No access to IsUserAnAdmin, so we assume there is no UAC + return true; + } + } + + //----------------// + // isCppInstalled // + //----------------// + @Override + public boolean isCppInstalled () + { + List output = new ArrayList(); + + try { + // Check Windows registry + final String radix = DescriptorFactory.WOW ? CPP.RADIX_WOW + : CPP.RADIX_PURE; + final String key = radix + + (DescriptorFactory.OS_ARCH.equals("x86") + ? CPP.KEY_32 : CPP.KEY_64); + int result = WindowsUtilities.queryRegistry( + output, + key, + "/v", + CPP.VALUE); + logger.debug("C++ query exit:{} output: {}", result, output); + + return result == 0; + } catch (Exception ex) { + logger.warn(Utilities.dumpOfLines(output)); + + return false; + } + } + + //------------------------// + // isGhostscriptInstalled // + //------------------------// + @Override + public boolean isGhostscriptInstalled () + { + return getGhostscriptPath() != null; + } + + //----------------------// + // isTesseractInstalled // + //----------------------// + @Override + public boolean isTesseractInstalled () + { + final String sysDir = DescriptorFactory.WOW ? TESS.SYSTEM_WOW + : TESS.SYSTEM_PURE; + + return Files.exists(Paths.get(sysDir, TESS.DLL_LEPTONICA)) + && Files.exists(Paths.get(sysDir, TESS.DLL_TESSERACT)) + && Files.exists(Paths.get(sysDir, TESS.DLL_BRIDGE)); + } + + //----------// + // runShell // + //----------// + @Override + public boolean runShell (boolean asAdmin, + List commands) + throws Exception + { + // Build a single compound command + StringBuilder sb = new StringBuilder(); + + for (String command : commands) { + if (sb.length() > 0) { + sb.append(" && "); + } + + sb.append(command); + } + + final String cmdLine = sb.toString(); + + if (asAdmin) { + final String cmdExe = System.getenv("ComSpec"); + WindowsUtilities.runElevated( + new File(cmdExe), + new File("."), + "/S", + "/E:ON", + "/C", + cmdLine); + } else { + List output = new ArrayList(); + int res = Utilities.runProcess( + output, + "cmd.exe", + "/c", + cmdLine); + + if (res != 0) { + final String lines = Utilities.dumpOfLines(output); + logger.warn(lines); + throw new RuntimeException("Failure in runShell().\n" + lines); + } + } + + return true; + } + + // //--------// + // // setenv // + // //--------// + // @Override + // public void setenv (boolean system, + // String var, + // String value) + // throws IOException, InterruptedException + // { + //// List args = new ArrayList<>(); + //// args.add("/C"); + //// args.add("setx"); + //// args.add(var); + //// args.add(value); + //// if (system) { + //// args.add("/M"); + //// } + //// List output = new ArrayList<>(); + //// Utilities.runProcess("cmd.exe", output, args.toArray(new String[args.size()])); + //// logger.debug("setenv output: {}", output); + // final List output = new ArrayList<>(); + // final String key = system ? ENV.SYSTEM_KEY : ENV.USER_KEY; + // WindowsUtilities.setRegistry(output, key, "/v", var, "/d", value); + // logger.debug("setenv output: {}", output); + // } + //---------------// + // setExecutable // + //---------------// + @Override + public void setExecutable (Path file) + throws Exception + { + // Void on Windows + } + + //--------------------// + // getGhostscriptPath // + //--------------------// + /** + * Retrieve the path to suitable ghostscript executable on Windows + * environments. + * + * This is implemented on registry informations, using CLI "reg" command: + * reg query "HKLM\SOFTWARE\GPL Ghostscript" /s + * + * @return the best suitable path, or null if nothing found + */ + private String getGhostscriptPath () + { + // Group names + final String VERSION = "version"; + final String PATH = "path"; + final String ARCH = "arch"; + + /** + * Regex for registry key line. + */ + final Pattern keyPattern = Pattern.compile( + "^HKEY_LOCAL_MACHINE\\\\SOFTWARE\\\\(Wow6432Node\\\\)?GPL Ghostscript\\\\" + + group(VERSION, "\\d+\\.\\d+") + "$"); + + /** + * Regex for registry value line. + */ + final Pattern valPattern = Pattern.compile( + "^\\s+GS_DLL\\s+REG_SZ\\s+" + group(PATH, ".+") + "$"); + + /** + * Regex for dll name. + */ + final Pattern dllPattern = Pattern.compile( + "gsdll" + group(ARCH, "\\d+") + "\\.dll$"); + + Double bestVersion = null; // Best version found so far + String bestPath = null; // Best path found so far + boolean relevant = false; // Is current registry info interesting? + int index = 0; // Line number in registry outputs + + double minVersion = Double.valueOf( + Descriptor.GHOSTSCRIPT_MIN_VERSION); + + // Browse registry output lines in sequence + for (String line : getRegistryGhostscriptOutputs()) { + logger.debug("Line#{}:{}", ++index, line); + + Matcher keyMatcher = keyPattern.matcher(line); + + if (keyMatcher.matches()) { + relevant = false; + + // Check version information + String versionStr = getGroup(keyMatcher, VERSION); + logger.debug("Version read as: {}", versionStr); + + Double version = Double.valueOf(versionStr); + + if ((version != null) && (version >= minVersion)) { + // We have an acceptable version + if ((bestVersion == null) || (bestVersion < version)) { + bestVersion = version; + logger.debug("Best version is: {}", versionStr); + relevant = true; + } else { + logger.debug("Version discarded: {}", versionStr); + } + } else { + logger.debug("Version unacceptable: {}", versionStr); + } + } else if (relevant) { + Matcher valMatcher = valPattern.matcher(line); + + if (valMatcher.matches()) { + // Read path information + bestPath = getGroup(valMatcher, PATH); + logger.debug("Best path is: {}", bestPath); + } + } + } + + // Extract prefix and dll from best path found, regardless of arch + if (bestPath != null) { + int lastSep = bestPath.lastIndexOf("\\"); + String prefix = bestPath.substring(0, lastSep); + logger.debug("Prefix is: {}", prefix); + + String dll = bestPath.substring(lastSep + 1); + logger.debug("Dll is: {}", dll); + + Matcher dllMatcher = dllPattern.matcher(dll); + + if (dllMatcher.matches()) { + String arch = getGroup(dllMatcher, ARCH); + String result = prefix + "\\gswin" + arch + "c.exe"; + logger.debug("Final path is: {}", result); + + return result; // Normal exit + } + } + + logger.info("Could not find suitable Ghostscript software"); + + return null; // Abnormal exit + } + + //-------------------// + // getLocalLibFolder // + //-------------------// + /** + * Report the local (temporary) folder where binary files are + * expanded before being copied to final target location. + * + * @return the local lib folder + */ + private File getLocalLibFolder () + { + return new File(getTempFolder(), LOCAL_LIB_FOLDER); + } + + //-------------------------------// + // getRegistryGhostscriptOutputs // + //-------------------------------// + /** + * Collect the output lines from registry queries about Ghostscript + * + * @return the output lines + */ + private List getRegistryGhostscriptOutputs () + { + /** + * Radices used in registry search (32, 64 or Wow). + */ + final String[] radices = new String[]{ + GS.RADIX_PURE, // Pure 32/32 or 64/64 + GS.RADIX_WOW // Wow (64/32) + }; + + // Access registry twice, one for win32 & win64 and one for Wow + List outputs = new ArrayList(); + + for (String radix : radices) { + logger.debug("Radix: {}", radix); + + try { + WindowsUtilities.queryRegistry( + outputs, + radix, + "/s"); + } catch (Exception ex) { + logger.error("Error in reading registry", ex); + } + } + + return outputs; + } + + //~ Inner Interfaces ------------------------------------------------------- + /** + * Data for Microsoft Visual C++ 2008 Redistributable. + */ + private static interface CPP + { + //~ Static fields/initializers ----------------------------------------- + + /** + * Registry value name. + */ + static final String VALUE = "DisplayName"; + + /** + * Registry radix for Wow (32/64). + */ + static final String RADIX_WOW = "HKLM\\SOFTWARE\\Wow6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\"; + + /** + * Registry radix for pure 32/32 or 64/64. + */ + static final String RADIX_PURE = "HKLM\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\"; + + /** + * Registry key for 32-bit. + */ + static final String KEY_32 = "{9BE518E6-ECC6-35A9-88E4-87755C07200F}"; + + /** + * Registry key for 64-bit. + */ + static final String KEY_64 = "{5FCE6D76-F5DC-37AB-B2B8-22AB8CEDB1D4}"; + + /** + * Download URL for 32-bit. + */ + static final String URL_32 = "http://download.microsoft.com/download/5/D/8/5D8C65CB-C849-4025-8E95-C3966CAFD8AE/vcredist_x86.exe"; + + /** + * Download URL for 64-bit. + */ + static final String URL_64 = "http://download.microsoft.com/download/5/D/8/5D8C65CB-C849-4025-8E95-C3966CAFD8AE/vcredist_x64.exe"; + + } + + /** + * For environment variables. + */ + private static interface ENV + { + //~ Static fields/initializers ----------------------------------------- + + /** + * Registry key for machine environment variable. + */ + static final String SYSTEM_KEY = "\"HKLM\\SYSTEM\\CurrentControlSet\\Control\\Session Manager\\Environment\""; + + /** + * Registry key for user environment variable. + */ + static final String USER_KEY = "HKCU\\Environment"; + + } + + /** + * Data for Ghostscript. + */ + private static interface GS + { + //~ Static fields/initializers ----------------------------------------- + + /** + * Registry radix for pure 32/32 or 64/64. + */ + static final String RADIX_PURE = "HKLM\\SOFTWARE\\GPL Ghostscript"; + + /** + * Registry radix for Wow (32/64). + */ + static final String RADIX_WOW = "HKLM\\SOFTWARE\\Wow6432Node\\GPL Ghostscript"; + + /** + * Download URL for 32-bit. + */ + static final String URL_32 = "http://downloads.ghostscript.com/public/gs907w32.exe"; + + /** + * Download URL for 64-bit. + */ + static final String URL_64 = "http://downloads.ghostscript.com/public/gs907w64.exe"; + + } + + /** + * Data for Tesseract. + */ + private static interface TESS + { + //~ Static fields/initializers ----------------------------------------- + + /** + * System location for pure 32/32 or 64/64. + */ + static final String SYSTEM_PURE = System.getenv("SystemRoot") + + "\\System32"; + + /** + * System location for Wow (32/64). + */ + static final String SYSTEM_WOW = System.getenv("SystemRoot") + + "\\SysWow64"; + + /** + * Jar name for 32-bit. + */ + static final String JAR_32 = "resources/tess-windows-32bit.jar"; + + /** + * Jar name for 64-bit. + */ + static final String JAR_64 = "resources/tess-windows-64bit.jar"; + + /** + * Dll for leptonica. + */ + static final String DLL_LEPTONICA = "liblept168.dll"; + + /** + * Dll for tesseract. + */ + static final String DLL_TESSERACT = "libtesseract302.dll"; + + /** + * Dll for bridge. + */ + static final String DLL_BRIDGE = "jniTessBridge.dll"; + + } +} diff --git a/src/installer/com/audiveris/installer/windows/WindowsUtilities.java b/src/installer/com/audiveris/installer/windows/WindowsUtilities.java new file mode 100644 index 0000000..9cefaf2 --- /dev/null +++ b/src/installer/com/audiveris/installer/windows/WindowsUtilities.java @@ -0,0 +1,157 @@ +//----------------------------------------------------------------------------// +// // +// W i n d o w s U t i l i t i e s // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package com.audiveris.installer.windows; + +import com.audiveris.installer.Utilities; +import com.sun.jna.Native; +import com.sun.jna.Pointer; + +import hudson.util.jna.Kernel32; +import hudson.util.jna.Kernel32Utils; +import hudson.util.jna.SHELLEXECUTEINFO; +import hudson.util.jna.Shell32; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * Class {@code WindowsUtilities} gathers utilities that are relevant + * for the Windows platform only. + * + * @author Hervé Bitteur + */ +public class WindowsUtilities +{ + //~ Static fields/initializers --------------------------------------------- + + /** Usual logger utility */ + private static final Logger logger = LoggerFactory.getLogger( + WindowsUtilities.class); + + //~ Methods ---------------------------------------------------------------- + //----------------// + // getCommandLine // + //----------------// + public static String getCommandLine () + { + return Kernel32.INSTANCE.GetCommandLineW() + .toString(); + } + + //-------------------// + // getModuleFilename // + //-------------------// + public static String getModuleFilename () + { + final int MAX_SIZE = 512; + byte[] exePathname = new byte[MAX_SIZE]; + Pointer zero = new Pointer(0); + int result = Kernel32.INSTANCE.GetModuleFileNameA( + zero, + exePathname, + MAX_SIZE); + + return Native.toString(exePathname); + } + + //-------------// + // runElevated // + //-------------// + public static int runElevated (File exec, + File pwd, + String... args) + throws IOException, InterruptedException + { + // Concatenate arguments into one string + StringBuilder sb = new StringBuilder(); + + for (String arg : args) { + sb.append(arg) + .append(" "); + } + + logger.debug("exec: {}", exec); + logger.debug("pwd: {}", pwd); + logger.debug("params: {}", sb); + logger.debug("Calling ShellExecuteEx..."); + + // error code 740 is ERROR_ELEVATION_REQUIRED, indicating that + // we run in UAC-enabled Windows and we need to run this in an elevated privilege + SHELLEXECUTEINFO sei = new SHELLEXECUTEINFO(); + sei.fMask = SHELLEXECUTEINFO.SEE_MASK_NOCLOSEPROCESS; + sei.lpVerb = "runas"; + sei.lpFile = exec.getAbsolutePath(); + sei.lpParameters = sb.toString(); + sei.lpDirectory = pwd.getAbsolutePath(); + sei.nShow = SHELLEXECUTEINFO.SW_HIDE; + + if (!Shell32.INSTANCE.ShellExecuteEx(sei)) { + throw new IOException( + "Failed to shellExecute: " + Native.getLastError()); + } + + try { + int result = Kernel32Utils.waitForExitProcess(sei.hProcess); + logger.debug("result: {}", result); + + return result; + } finally { + // TODO: need to print content of stdout/stderr + // FileInputStream fin = new FileInputStream( + // new File(pwd, "redirect.log")); + // IOUtils.copy(fin, out.getLogger()); + // fin.close(); + } + } + + //---------------// + // queryRegistry // + //---------------// + public static int queryRegistry (List output, + String... args) + throws IOException, InterruptedException + { + // Command arguments + List cmdArgs = new ArrayList<>(); + cmdArgs.add("cmd.exe"); + cmdArgs.addAll(Arrays.asList("/c", "reg", "query")); + cmdArgs.addAll(Arrays.asList(args)); + logger.debug("cmd query: {}", cmdArgs); + + return Utilities.runProcess(output, + cmdArgs.toArray(new String[cmdArgs.size()])); + } + + //-------------// + // setRegistry // + //-------------// + public static int setRegistry (List output, + String... args) + throws IOException, InterruptedException + { + // Command arguments + List cmdArgs = new ArrayList<>(); + cmdArgs.add("cmd.exe"); + cmdArgs.addAll(Arrays.asList("/c", "reg", "add")); + cmdArgs.addAll(Arrays.asList(args)); + logger.debug("cmd add: {}", cmdArgs); + + return Utilities.runProcess(output, + cmdArgs.toArray(new String[cmdArgs.size()])); + } +} diff --git a/src/installer/com/audiveris/installer/windows/package.html b/src/installer/com/audiveris/installer/windows/package.html new file mode 100644 index 0000000..9a25208 --- /dev/null +++ b/src/installer/com/audiveris/installer/windows/package.html @@ -0,0 +1,14 @@ + + + + + Package com.audiveris.installer.windows + + + +

+ Specific implementations for Windows environment. +

+ + + diff --git a/src/installer/doc-files/overview.uxf b/src/installer/doc-files/overview.uxf new file mode 100644 index 0000000..4afbb7b --- /dev/null +++ b/src/installer/doc-files/overview.uxf @@ -0,0 +1,499 @@ + + + 10 + + com.umlet.element.Class + + 10 + 330 + 130 + 160 + + <<factory>> +*DescriptorFactory* +bg=#888888 +-- +OS_NAME +OS_ARCH +LINUX +MAC_OS_X +WINDOWS +-- +getDescriptor() + + + + com.umlet.element.Class + + 180 + 420 + 80 + 50 + + <<interface>> +bg=#bbbbbb +*Descriptor* + + + + com.umlet.element.Relation + + 620 + 330 + 50 + 250 + + lt=<<- + 30;30;30;230 + + + com.umlet.element.Relation + + 510 + 90 + 110 + 50 + + lt=<<<<-> + 30;30;90;30 + + + com.umlet.element.Class + + 10 + 10 + 200 + 260 + + <<facade>> +*Jnlp* +bg=#ffeedd +-- +*basicService:* +-- ++getCodebase() ++isWebBrowserSupported() ++showDocument() +-- +*extensionInstallerService:* +-- ++setHeading() ++setStatus() ++updateProgress() ++installSucceeded() ++installFailed() +-- +*downloadService:* +(unused) + + + + com.umlet.element.Class + + 600 + 100 + 120 + 120 + + <<interface>> +*Companion* +bg=#aaffaa +-- +-- +isNeeded() +checkInstalled() +install() +uninstall() + + + + com.umlet.element.Class + + 390 + 400 + 150 + 30 + + LicenseCompanion + + + + com.umlet.element.Class + + 510 + 560 + 150 + 30 + + DocCompanion + + + + com.umlet.element.Relation + + 20 + 550 + 120 + 50 + + lt=- + 30;30;100;30 + + + com.umlet.element.Class + + 80 + 530 + 130 + 30 + + WindowsDescriptor + + + + com.umlet.element.Class + + 600 + 10 + 120 + 50 + + *CompanionView* +bg=#eeeeff +-- +-- +update() + + + + com.umlet.element.Relation + + 630 + 30 + 50 + 90 + + lt=- + 30;30;30;70 + + + com.umlet.element.Class + + 520 + 260 + 230 + 100 + + /AbstractCompanion/ +-- +-- +/#doInstall()/ +#doUninstall() +#getTargetFolder() +#makeTargetFolder() + + + + com.umlet.element.Relation + + 210 + 440 + 50 + 190 + + lt=<<. + 30;30;30;170 + + + com.umlet.element.Relation + + 590 + 330 + 50 + 210 + + lt=<<- + 30;30;30;190 + + + com.umlet.element.Class + + 120 + 570 + 110 + 30 + + UnixDescriptor + + + + com.umlet.element.Relation + + 650 + 330 + 50 + 290 + + lt=<<- + 30;30;30;270 + + + com.umlet.element.Relation + + 170 + 440 + 50 + 110 + + lt=<<. + 30;30;30;90 + + + com.umlet.element.Class + + 420 + 440 + 150 + 30 + + CppCompanion + + + + com.umlet.element.Relation + + 630 + 190 + 50 + 90 + + lt=<<. + 30;30;30;70 + + + com.umlet.element.Relation + + 320 + 90 + 110 + 50 + + lt=- + 30;30;90;30 + + + com.umlet.element.Class + + 410 + 10 + 130 + 50 + + *BundleView* +bg=#eeeeff +-- +-- +publishMessage() + + + + com.umlet.element.Relation + + 20 + 460 + 140 + 180 + + lt=<<<<- + 30;30;30;160;120;160 + + + com.umlet.element.Class + + 600 + 680 + 150 + 30 + + TrainingCompanion + + + + com.umlet.element.Class + + 410 + 100 + 130 + 70 + + *Bundle* +bg=#ffffcc +-- +-- +installBundle() +uninstallBundle() + + + + com.umlet.element.Relation + + 680 + 330 + 50 + 330 + + lt=<<- + 30;30;30;310 + + + com.umlet.element.Class + + 540 + 600 + 150 + 30 + + ExamplesCompanion + + + + com.umlet.element.Class + + 140 + 610 + 110 + 30 + + MacDescriptor + + + + com.umlet.element.Relation + + 560 + 330 + 50 + 170 + + lt=<<- + 30;30;30;150 + + + com.umlet.element.Relation + + 190 + 440 + 50 + 150 + + lt=<<. + 30;30;30;130 + + + com.umlet.element.Class + + 570 + 640 + 150 + 30 + + PluginsCompanion + + + + com.umlet.element.Class + + 480 + 520 + 150 + 30 + + OcrCompanion + + + + com.umlet.element.Relation + + 710 + 330 + 50 + 370 + + lt=<<- + 30;30;30;350 + + + com.umlet.element.Relation + + 450 + 30 + 50 + 90 + + lt=- + 30;30;30;70 + + + com.umlet.element.Relation + + 500 + 330 + 50 + 90 + + lt=<<- + 30;30;30;70 + + + com.umlet.element.Relation + + 20 + 510 + 80 + 50 + + lt=- + 30;30;60;30 + + + com.umlet.element.Relation + + 530 + 330 + 50 + 130 + + lt=<<- + 30;30;30;110 + + + com.umlet.element.Class + + 450 + 480 + 150 + 30 + + GhostscriptCompanion + + + + com.umlet.element.Class + + 250 + 100 + 100 + 120 + + *Installer* +bg=#ffaaaa +-- +-- +getBundle() +getFrame() +main() +install() +uninstall() + + + diff --git a/src/installer/doc-files/roles.uxf b/src/installer/doc-files/roles.uxf new file mode 100644 index 0000000..11f7b59 --- /dev/null +++ b/src/installer/doc-files/roles.uxf @@ -0,0 +1,585 @@ + + + 10 + + com.umlet.element.custom.Database + + 480 + 270 + 110 + 40 + + launch.jnlp + + + + com.umlet.element.Class + + 860 + 160 + 200 + 30 + + *javaws.exe (64)* +bg=#88ff88 + + + + com.umlet.element.Class + + 10 + 160 + 240 + 30 + + *javaws.exe (32)* +bg=#ff8888 + + + + com.umlet.element.custom.Database + + 480 + 350 + 110 + 40 + + audiveris.jar + + + + com.umlet.element.custom.Database + + 480 + 430 + 110 + 40 + + ... + + + + com.umlet.element.custom.Database + + 360 + 630 + 160 + 100 + + *tess-windows-32bit.jar* +bg=#ff8888 +-- +jniTessBridge.dll +libtesseract302.dll +liblept168.dll +fg=#ff8888 + + + + com.umlet.element.custom.Database + + 470 + 740 + 130 + 40 + + documentation.jar + + + + com.umlet.element.custom.Database + + 480 + 780 + 110 + 40 + + train.jar + + + + com.umlet.element.Relation + + 220 + 120 + 300 + 70 + + lt=<- +? + 30;50;280;50 + + + com.umlet.element.Relation + + 540 + 120 + 340 + 70 + + lt=-> +? + 30;50;320;50 + + + com.umlet.element.custom.Database + + 480 + 820 + 110 + 40 + + examples.jar + + + + com.umlet.element.custom.Database + + 480 + 860 + 110 + 40 + + specifics.jar + + + + com.umlet.element.custom.Database + + 480 + 900 + 110 + 40 + + plugins.jar + + + + com.umlet.element.Class + + 740 + 1020 + 320 + 30 + + C++ runtime (64) +bg=#88ff88 + + + + com.umlet.element.Class + + 10 + 1020 + 320 + 30 + + C++ runtime (32) +bg=#ff8888 + + + + com.umlet.element.Class + + 10 + 660 + 170 + 60 + + jniTessBridge.dll (32) +libtesseract302.dll (32) +liblept168.dll (32) +bg=#ff8888 + + + + com.umlet.element.Class + + 900 + 660 + 160 + 60 + + jniTessBridge.dll (64) +libtesseract302.dll (64) +liblept168.dll (64) +bg=#88ff88 + + + + com.umlet.element.custom.Database + + 550 + 630 + 160 + 100 + + *tess-windows-64bit.jar* +-- +jniTessBridge.dll +libtesseract302.dll +liblept168.dll +fg=#88ff88 + + + + com.umlet.element.custom.Database + + 480 + 510 + 110 + 40 + + installer.jar + + + + com.umlet.element.custom.Database + + 480 + 470 + 110 + 40 + + tesseract-3.jar + + + + com.umlet.element.Package + + 350 + 600 + 370 + 350 + + resources (for Installer) +bg=#aaffff + + + + com.umlet.element.Package + + 460 + 400 + 150 + 160 + + lib + + + + com.umlet.element.Package + + 390 + 1090 + 290 + 160 + + Google site +bg=#cccccc + + + + com.umlet.element.custom.Database + + 440 + 1120 + 190 + 40 + + tesseract-ocr-3.02.eng.tar.gz + + + + com.umlet.element.custom.Database + + 440 + 1160 + 190 + 40 + + tesseract-ocr-3.02.ita.tar.gz + + + + com.umlet.element.custom.Database + + 440 + 1200 + 190 + 40 + + ... + + + + com.umlet.element.Class + + 860 + 190 + 200 + 30 + + c:\Program Files\Java\jre7\bin\ + + + + com.umlet.element.Class + + 740 + 1050 + 320 + 30 + + Microsoft Visual C++ 2008 Redistributable x64 9.0 + + + + com.umlet.element.Class + + 10 + 1050 + 320 + 30 + + Microsoft Visual C++ 2008 Redistributable x86 9.0 + + + + UMLClass + + 900 + 720 + 160 + 30 + + c:\Windows\System32\ +valign=center + + + + UMLClass + + 10 + 720 + 170 + 30 + + c:\Windows\SysWOW64\ +valign=center + + + + com.umlet.element.Class + + 10 + 190 + 240 + 30 + + c:\Program Files (x86)\Java\jre7\bin\ + + + + com.umlet.element.Relation + + 680 + 610 + 240 + 70 + + lt=- +copy> + 30;50;220;50 + + + com.umlet.element.Relation + + 150 + 610 + 230 + 70 + + lt=- +<copy + 30;50;210;50 + + + com.umlet.element.Package + + 390 + 980 + 290 + 80 + + Microsoft site +bg=#cccccc + + + + com.umlet.element.custom.Database + + 400 + 1010 + 120 + 40 + + *vcredist_x86.exe* +fg=#ff8888 + + + + com.umlet.element.custom.Database + + 550 + 1010 + 120 + 40 + + *vcredist_x64.exe* +fg=#88ff88 + + + + com.umlet.element.Relation + + 640 + 980 + 120 + 70 + + lt=- +install> + 30;50;100;50 + + + com.umlet.element.Relation + + 300 + 980 + 120 + 70 + + lt=- +<install + 30;50;100;50 + + + com.umlet.element.custom.Database + + 480 + 310 + 110 + 40 + + installer.jnlp + + + + com.umlet.element.Package + + 440 + 240 + 190 + 330 + + kept in java cache +bg=#ffffaa + + + + UMLActor + + 500 + 10 + 60 + 100 + + user +bg=red + + + + com.umlet.element.custom.State + + 480 + 110 + 110 + 40 + + web browser +bg=#8888ff + + + + com.umlet.element.Relation + + 220 + 100 + 280 + 90 + + lt=<- +? + 30;70;260;30 + + + com.umlet.element.Relation + + 560 + 100 + 320 + 90 + + lt=-> +? + 30;30;300;70 + + + com.umlet.element.custom.State + + 500 + 160 + 70 + 20 + + shortcut +bg=#ff0000 + + + + com.umlet.element.custom.State + + 480 + 190 + 110 + 20 + + command line + + + + com.umlet.element.Relation + + 560 + 140 + 320 + 80 + + lt=-> +? + 30;60;300;30 + + + com.umlet.element.Relation + + 220 + 140 + 280 + 80 + + lt=<- +? + 30;30;260;60 + + diff --git a/src/installer/hudson/util/jna/InitializationErrorInvocationHandler.java b/src/installer/hudson/util/jna/InitializationErrorInvocationHandler.java new file mode 100644 index 0000000..ea48d46 --- /dev/null +++ b/src/installer/hudson/util/jna/InitializationErrorInvocationHandler.java @@ -0,0 +1,58 @@ + +package hudson.util.jna; + +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; + +/** + * {@link InvocationHandler} that reports the same exception over and over again when methods are invoked + * on the interface. + * + * This is convenient to remember why the initialization of the real JNA proxy failed. + * + * @author Kohsuke Kawaguchi + * @since 1.487 + * @see Related bug report against JDK + */ +public class InitializationErrorInvocationHandler + implements InvocationHandler +{ + //~ Instance fields -------------------------------------------------------- + + private final Throwable cause; + + //~ Constructors ----------------------------------------------------------- + + private InitializationErrorInvocationHandler (Throwable cause) + { + this.cause = cause; + } + + //~ Methods ---------------------------------------------------------------- + + public static T create (Class type, + Throwable cause) + { + return type.cast( + Proxy.newProxyInstance( + type.getClassLoader(), + new Class[] { type }, + new InitializationErrorInvocationHandler(cause))); + } + + @Override + public Object invoke (Object proxy, + Method method, + Object[] args) + throws Throwable + { + if (method.getDeclaringClass() == Object.class) { + return method.invoke(this, args); + } + + throw new UnsupportedOperationException( + "Failed to link the library: " + method.getDeclaringClass(), + cause); + } +} diff --git a/src/installer/hudson/util/jna/Kernel32.java b/src/installer/hudson/util/jna/Kernel32.java new file mode 100644 index 0000000..80e2f44 --- /dev/null +++ b/src/installer/hudson/util/jna/Kernel32.java @@ -0,0 +1,93 @@ +/* + * The MIT License + * + * Copyright (c) 2004-2009, Sun Microsystems, Inc., Kohsuke Kawaguchi, CloudBees, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package hudson.util.jna; + +import com.sun.jna.Pointer; +import com.sun.jna.WString; +import com.sun.jna.ptr.IntByReference; +import com.sun.jna.win32.StdCallLibrary; + +/** + * JNA interface to Windows Kernel32 exports. + * + * @author Kohsuke Kawaguchi + */ +public interface Kernel32 + extends StdCallLibrary +{ + //~ Instance fields -------------------------------------------------------- + + Kernel32 INSTANCE = Kernel32Utils.load(); + int MOVEFILE_COPY_ALLOWED = 2; + int MOVEFILE_CREATE_HARDLINK = 16; + int MOVEFILE_DELAY_UNTIL_REBOOT = 4; + int MOVEFILE_FAIL_IF_NOT_TRACKABLE = 32; + int MOVEFILE_REPLACE_EXISTING = 1; + int MOVEFILE_WRITE_THROUGH = 8; + int FILE_ATTRIBUTE_REPARSE_POINT = 0x400; + int SYMBOLIC_LINK_FLAG_DIRECTORY = 1; + int STILL_ACTIVE = 259; + + //~ Methods ---------------------------------------------------------------- + + /** HB */ + WString GetCommandLineW(); + + /** HB */ + int GetModuleFileNameA (Pointer hModule, byte[] lpFilename, int nSize); + + /** + * Creates a symbolic link. + * + * Windows Vista+, Windows Server 2008+ + * + * @param lpSymlinkFileName + * Symbolic link to be created + * @param lpTargetFileName + * Target of the link. + * @param dwFlags + * 0 or {@link #SYMBOLIC_LINK_FLAG_DIRECTORY} + * @see MSDN + */ + boolean CreateSymbolicLinkW (WString lpSymlinkFileName, + WString lpTargetFileName, + int dwFlags); + + boolean GetExitCodeProcess (Pointer handle, + IntByReference r); + + int GetFileAttributesW (WString lpFileName); + + /** + * See http://msdn.microsoft.com/en-us/library/aa365240(VS.85).aspx + */ + boolean MoveFileExA (String existingFileName, + String newFileName, + int flags); + + int WaitForSingleObject (Pointer handle, + int milliseconds); + + // DWORD == int +} diff --git a/src/installer/hudson/util/jna/Kernel32Utils.java b/src/installer/hudson/util/jna/Kernel32Utils.java new file mode 100644 index 0000000..cd509ee --- /dev/null +++ b/src/installer/hudson/util/jna/Kernel32Utils.java @@ -0,0 +1,136 @@ +/* + * The MIT License + * + * Copyright (c) 2010, CloudBees, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package hudson.util.jna; + +import com.sun.jna.Native; +import com.sun.jna.Pointer; +import com.sun.jna.WString; +import com.sun.jna.ptr.IntByReference; + +import java.io.File; +import java.io.IOException; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * + * @author Kohsuke Kawaguchi + */ +public class Kernel32Utils +{ + //~ Static fields/initializers --------------------------------------------- + + private static final Logger LOGGER = Logger.getLogger( + Kernel32Utils.class.getName()); + + //~ Methods ---------------------------------------------------------------- + /** + * @param target + * If relative, resolved against the location of the symlink. + * If absolute, it's absolute. + */ + public static void createSymbolicLink (File symlink, + String target, + boolean dirLink) + throws IOException + { + if (!Kernel32.INSTANCE.CreateSymbolicLinkW( + new WString(symlink.getPath()), + new WString(target), + dirLink ? Kernel32.SYMBOLIC_LINK_FLAG_DIRECTORY : 0)) { + throw new WinIOException( + "Failed to create a symlink " + symlink + " to " + target); + } + } + + public static int getWin32FileAttributes (File file) + throws IOException + { + // allow lookup of paths longer than MAX_PATH + // http://msdn.microsoft.com/en-us/library/aa365247(v=VS.85).aspx + String canonicalPath = file.getCanonicalPath(); + String path; + + if (canonicalPath.length() < 260) { + // path is short, use as-is + path = canonicalPath; + } else if (canonicalPath.startsWith("\\\\")) { + // network share + // \\server\share --> \\?\UNC\server\share + path = "\\\\?\\UNC\\" + canonicalPath.substring(2); + } else { + // prefix, canonical path should be normalized and absolute so this should work. + path = "\\\\?\\" + canonicalPath; + } + + return Kernel32.INSTANCE.GetFileAttributesW(new WString(path)); + } + + public static boolean isJunctionOrSymlink (File file) + throws IOException + { + return (file.exists() + && ((Kernel32.FILE_ATTRIBUTE_REPARSE_POINT + & getWin32FileAttributes(file)) != 0)); + } + + /** + * Given the process handle, waits for its completion and returns the exit + * code. + */ + public static int waitForExitProcess (Pointer hProcess) + throws InterruptedException + { + while (true) { + if (Thread.interrupted()) { + throw new InterruptedException(); + } + + Kernel32.INSTANCE.WaitForSingleObject(hProcess, 1000); + + IntByReference exitCode = new IntByReference(); + exitCode.setValue(-1); + Kernel32.INSTANCE.GetExitCodeProcess(hProcess, exitCode); + + int v = exitCode.getValue(); + + if (v != Kernel32.STILL_ACTIVE) { + return v; + } + } + } + + /* package */ static Kernel32 load () + { + try { + return (Kernel32) Native.loadLibrary("kernel32", Kernel32.class); + } catch (Throwable e) { + LOGGER.log(Level.SEVERE, "Failed to load Kernel32", e); + + return InitializationErrorInvocationHandler.create( + Kernel32.class, + e); + } + } +} diff --git a/src/installer/hudson/util/jna/SHELLEXECUTEINFO.java b/src/installer/hudson/util/jna/SHELLEXECUTEINFO.java new file mode 100644 index 0000000..0f9062e --- /dev/null +++ b/src/installer/hudson/util/jna/SHELLEXECUTEINFO.java @@ -0,0 +1,80 @@ +//----------------------------------------------------------------------------// +// // +// S H E L L E X E C U T E I N F O // +// // +//----------------------------------------------------------------------------// +package hudson.util.jna; + +import com.sun.jna.Pointer; +import com.sun.jna.Structure; + +import java.util.Arrays; +import java.util.List; + +/** + * + *
+       typedef struct _SHELLEXECUTEINFO {
+         DWORD     cbSize;
+         ULONG     fMask;
+         HWND      hwnd;
+         LPCTSTR   lpVerb;
+         LPCTSTR   lpFile;
+         LPCTSTR   lpParameters;
+         LPCTSTR   lpDirectory;
+         int       nShow;
+         HINSTANCE hInstApp;
+         LPVOID    lpIDList;
+         LPCTSTR   lpClass;
+         HKEY      hkeyClass;
+         DWORD     dwHotKey;
+         union {
+           HANDLE hIcon;
+           HANDLE hMonitor;
+         } DUMMYUNIONNAME;
+         HANDLE    hProcess;
+       } SHELLEXECUTEINFO, *LPSHELLEXECUTEINFO;
+ * 
+ * @author Kohsuke Kawaguchi + * see http://msdn.microsoft.com/en-us/library/windows/desktop/bb759784%28v=vs.85%29.aspx + */ +public class SHELLEXECUTEINFO + extends Structure +{ + //~ Static fields/initializers --------------------------------------------- + + public static final int SEE_MASK_NOCLOSEPROCESS = 0x40; + public static final int SW_HIDE = 0; + public static final int SW_SHOW = 0; + + //~ Instance fields -------------------------------------------------------- + + public int cbSize = size(); + public int fMask; + public Pointer hwnd; + public String lpVerb; + public String lpFile; + public String lpParameters; + public String lpDirectory; + public int nShow = 1; + public Pointer hInstApp; + public Pointer lpIDList; + public String lpClass; + public Pointer hkeyClass; + public int dwHotKey; + public Pointer hIcon; + public Pointer hProcess; + + //~ Methods ---------------------------------------------------------------- + + @Override + protected List getFieldOrder () + { + return Arrays.asList( + new String[] { + "cbSize", "fMask", "hwnd", "lpVerb", "lpFile", "lpParameters", + "lpDirectory", "nShow", "hInstApp", "lpIDList", "lpClass", + "hkeyClass", "dwHotKey", "hIcon", "hProcess" + }); + } +} diff --git a/src/installer/hudson/util/jna/Shell32.java b/src/installer/hudson/util/jna/Shell32.java new file mode 100644 index 0000000..c019a69 --- /dev/null +++ b/src/installer/hudson/util/jna/Shell32.java @@ -0,0 +1,44 @@ +/* + * The MIT License + * + * Copyright (c) 2010, CloudBees, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package hudson.util.jna; + +import com.sun.jna.Native; +import com.sun.jna.win32.StdCallLibrary; + +/** + * @author Kohsuke Kawaguchi + */ +public interface Shell32 extends StdCallLibrary { + Shell32 INSTANCE = (Shell32) Native.loadLibrary("shell32", Shell32.class); + + /** + * @return true if successful. Otherwise false. + */ + boolean ShellExecuteEx(SHELLEXECUTEINFO lpExecInfo); + + /** + * @return true if successful. Otherwise false. + */ + boolean IsUserAnAdmin(); +} diff --git a/src/installer/hudson/util/jna/WinIOException.java b/src/installer/hudson/util/jna/WinIOException.java new file mode 100644 index 0000000..034853e --- /dev/null +++ b/src/installer/hudson/util/jna/WinIOException.java @@ -0,0 +1,76 @@ + +package hudson.util.jna; + +import com.sun.jna.Native; + +///import hudson.Util; +import java.io.IOException; + +/** + * IOException originated from Windows API call. + * + * @author Kohsuke Kawaguchi + */ +public class WinIOException + extends IOException +{ + //~ Instance fields -------------------------------------------------------- + + private final int errorCode = Native.getLastError(); + + //~ Constructors ----------------------------------------------------------- + + /** + * Creates a new WinIOException object. + */ + public WinIOException () + { + } + + /** + * Creates a new WinIOException object. + * + * @param message DOCUMENT ME! + */ + public WinIOException (String message) + { + super(message); + } + + /** + * Creates a new WinIOException object. + * + * @param message DOCUMENT ME! + * @param cause DOCUMENT ME! + */ + public WinIOException (String message, + Throwable cause) + { + super(message); + initCause(cause); + } + + /** + * Creates a new WinIOException object. + * + * @param cause DOCUMENT ME! + */ + public WinIOException (Throwable cause) + { + initCause(cause); + } + + //~ Methods ---------------------------------------------------------------- + + public int getErrorCode () + { + return errorCode; + } + + @Override + public String getMessage () + { + ///return super.getMessage()+" error="+errorCode+":"+ Util.getWin32ErrorMessage(errorCode); + return super.getMessage() + " error=" + errorCode; + } +} diff --git a/src/installer/hudson/util/jna/package.html b/src/installer/hudson/util/jna/package.html new file mode 100644 index 0000000..75eb283 --- /dev/null +++ b/src/installer/hudson/util/jna/package.html @@ -0,0 +1,14 @@ + + + + + Package hudson.util.jna + + + +

+ JNA utilities meant for native access. +

+ + + diff --git a/src/installer/overview.html b/src/installer/overview.html new file mode 100644 index 0000000..1a96c97 --- /dev/null +++ b/src/installer/overview.html @@ -0,0 +1,127 @@ + + + + Audiveris Installer overview + + + +

Application and installer

+

+ Audiveris is now made available through the Java Web Start + technology. + Any launch of Audiveris is thus performed through a program called + javaws which is provided with launch.jnlp, + a file which describes how Audiveris is to be launched. +

+

+ The very first time Audiveris is launched, a specific companion of + Audiveris (named "installer") is run once before the application is + launched. The following times, only the application is launched. + The purpose of this installer is to check the user environment and + install missing components as needed (additional software and data). +

+

+ The following diagram depicts how this is "orchestrated" by + javaws. + Only the relevant components are shown, especially to point out the + differences implied by the fact that the chosen javaws is a 32-bit + or a 64-bit version. + In the diagram, the left column refers to 32-bit Java and the right + column to 64-bit Java. +

+

+ The specific version of javaws can be chosen: +

+
    +
  • + From the command line when selecting javaws from + proper Java environment.
  • +
  • + From Audiveris desktop shortcut. + This depends on which javaws file the Audiveris.lnk file points + to, and can be changed by modifying the shortcut properties.
  • +
  • + Implicitly when clicking on the "Launch" button of Audiveris + web site: it's up to the web browser to go the 32 or 64 route. +
  • +
+

+ + The corresponding file locations are typically those shown by + Windows 7, 64-bit, where both Java environments have been installed: + "Program Files (x86)" vs "Program Files", + "Windows\SysWOW64" vs "Windows\System32", etc. + Roughly speaking, on a 32-bit Windows version (which can run only + a 32-bit Java environment), the 32-bit folders have the names of the + corresponding 64-bit folders on a 64-bit Windows. +

+ + +

Internal installer architecture

+

+ The Audiveris installer is organized around one core package + (com.audiveris.installer) and as many sub-packages as needed to + address different OSes. +

+ + + +

Descriptors to support OS'es

+ +

+ The bulk of Installer processing is handled by classes from + com.audiveris.installer package, especially: +

+
    +
  • Installer + the main class which drives the installation or uninstallation,
  • +
  • Bundle + which handles the collection of companions to process,
  • +
  • Companion + which handles the installation/uninstallation of a given software + companion.
  • +
+ +

+ The specificities of a particular OS environment are meant to be + implemented in a proper sub-class of + Descriptor class. +
+ For example, WindowsDescriptor + is the sub-class that implements the Windows descriptor. +

+ +

+ The Descriptor class has been carefully designed, and should be the + only source to be extended by subclassing, to support OS'es like + Unix or Mac. +

+ +

Jnlp to encapsulate JNLP services

+ +

+ The Jnlp class is + meant to hide the underlying Java Web start environment and also to + allow the running and debugging of the Installer even without any + Java Web Start environment available underneath. +

+ +

+ It provides a selection of JNLP services: +

+ +
    +
  • BasicService for access to code base and web browser.
  • +
  • ExtensionInstallerService for reporting progress of the + Installer to the Java Web Start,
  • +
  • DownloadService which should be used to monitor downloading. + However, it requires all potentially downloadable URLs to be + explicitly referred to by the JNLP file and is also said to be + limited to the download of .jar files + (while Tesseract language files have a .tar.gz extension). + To be further investigated.
  • +
+ + + + \ No newline at end of file diff --git a/src/main/Audiveris.java b/src/main/Audiveris.java new file mode 100644 index 0000000..4c8890b --- /dev/null +++ b/src/main/Audiveris.java @@ -0,0 +1,49 @@ +//----------------------------------------------------------------------------// +// // +// A u d i v e r i s // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +import omr.WellKnowns; + +/** + * Class {@code Audiveris} is simply the entry point to OMR, which + * delegates the call to {@link omr.Main#doMain}. + * + * @author Hervé Bitteur + */ +public final class Audiveris +{ + //~ Constructors ----------------------------------------------------------- + + //-----------// + // Audiveris // + //-----------// + /** To avoid instantiation */ + private Audiveris () + { + } + + //~ Methods ---------------------------------------------------------------- + //------// + // main // + //------// + /** + * The main entry point, which just calls {@link omr.Main#doMain}. + * + * @param args These args are simply passed to Main + */ + public static void main (final String[] args) + { + // We need class WellKnowns to be elaborated before class Main + WellKnowns.ensureLoaded(); + + // Then we call Main... + omr.Main.doMain(args); + } +} diff --git a/src/main/omr/CLI.java b/src/main/omr/CLI.java new file mode 100644 index 0000000..1c58b1f --- /dev/null +++ b/src/main/omr/CLI.java @@ -0,0 +1,676 @@ +//----------------------------------------------------------------------------// +// // +// C L I // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr; + +import omr.step.Step; +import omr.step.Steps; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.BufferedReader; +import java.io.FileNotFoundException; +import java.io.FileReader; +import java.io.IOException; +import java.io.StringReader; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Locale; +import java.util.Properties; +import java.util.Set; +import java.util.SortedSet; +import java.util.TreeSet; + +/** + * Class {@code CLI} parses and holds the parameters of the command + * line interface. + * + *

The command line parameters can be (order and case are not relevant): + *

+ * + *
-help
to print a quick usage help and leave the + * application.
+ * + *
-batch
to run in batch mode, with no user + * interface.
+ * + *
-step (STEPNAME | @STEPLIST)+
to run all the + * specified steps (including the steps which are mandatory to get to the + * specified ones). 'STEPNAME' can be any one of the step names (the case is + * irrelevant) as defined in the {@link Steps} class. These steps will + * be performed on each sheet referenced from the command line.
+ * + *
-option (KEY=VALUE | @OPTIONLIST)+
to specify + * the value of some application parameters (that can also be set via the + * pull-down menu "Tools|Options"), either by stating the key=value pair or by + * referencing (flagged by a @ sign) a file that lists key=value pairs (or + * even other files list recursively). + * A list file is a simple text file, with one key=value pair per line. + * Nota: The syntax used is the Properties syntax, so for example + * back-slashes must be escaped.
+ * + *
-script (SCRIPTNAME | @SCRIPTLIST)+
to specify + * some scripts to be read, using the same mechanism than input command belows. + * These script files contain actions generally recorded during a previous run. + *
+ * + *
-input (FILENAME | @FILELIST)+
to specify some + * image files to be read, either by naming the image file or by referencing + * (flagged by a @ sign) a file that lists image files (or even other files + * list recursively). + * A list file is a simple text file, with one image file name per line.
+ * + *
-pages (PAGE | @PAGELIST)+
to specify some + * specific pages, counted from 1, to be loaded out of the input file.
+ * + *
-bench (DIRNAME | FILENAME)
to define an output + * path to bench data file (or directory). + * Nota: If the path refers to an existing directory, each processed + * score will output its bench data to a score-specific file created in the + * provided directory. Otherwise, all bench data, whatever its related score, + * will be written to the provided single file.
+ * + * + *
-midi (DIRNAME | FILENAME)
to define an output + * path to MIDI file (or directory). Same note as for -bench.
+ *
+ * + *
-print (DIRNAME | FILENAME)
to define an output + * path to PDF file (or directory). Same note as for -bench.
+ * + *
-export (DIRNAME | FILENAME)
to define an output + * path to MusicXML file (or directory). Same note as for -bench.
+ * + *
+ * + * @author Hervé Bitteur + */ +public class CLI +{ + //~ Static fields/initializers --------------------------------------------- + + /** Usual logger utility */ + private static final Logger logger = LoggerFactory.getLogger(CLI.class); + + //~ Enumerations ----------------------------------------------------------- + /** For handling cardinality of command parameters */ + private static enum Card + { + //~ Enumeration constant initializers ---------------------------------- + + /** No parameter expected */ + NONE, + /** Just a single parameter is + * expected */ + SINGLE, + /** One or several + * parameters are expected */ + MULTIPLE; + + } + + /** For command analysis */ + private static enum Command + { + //~ Enumeration constant initializers ---------------------------------- + + HELP( + "Prints help about application arguments and stops", + Card.NONE, + null), + BATCH( + "Specifies to run with no graphic user interface", + Card.NONE, + null), + STEP( + "Defines a series of target steps", + Card.MULTIPLE, + "(STEPNAME|@STEPLIST)+"), + OPTION( + "Defines a series of key=value constant pairs", + Card.MULTIPLE, + "(KEY=VALUE|@OPTIONLIST)+"), + SCRIPT( + "Defines a series of script files to run", + Card.MULTIPLE, + "(SCRIPTNAME|@SCRIPTLIST)+"), + INPUT( + "Defines a series of input image files to process", + Card.MULTIPLE, + "(FILENAME|@FILELIST)+"), + PAGES( + "Defines a set of specific pages to process", + Card.MULTIPLE, + "(PAGE|@PAGELIST)+"), + BENCH( + "Defines an output path to bench data file (or directory)", + Card.SINGLE, + "(DIRNAME|FILENAME)"), + // MIDI( + // "Defines an output path to MIDI file (or directory)", + // Card.SINGLE, + // "(DIRNAME|FILENAME)"), + PRINT( + "Defines an output path to PDF file (or directory)", + Card.SINGLE, + "(DIRNAME|FILENAME)"), + EXPORT( + "Defines an output path to MusicXML file (or directory)", + Card.SINGLE, + "(DIRNAME|FILENAME)"); + //~ Instance fields ---------------------------------------------------- + + /** Info about command itself */ + public final String description; + + /** Cardinality of the expected parameters */ + public final Card card; + + /** Info about expected command parameters */ + public final String params; + + //~ Constructors ------------------------------------------------------- + /** + * Creates a Command object. + * + * @param description description of the command + * @param params description of command expected parameters, or + * null + */ + Command (String description, + Card card, + String params) + { + this.description = description; + this.card = card; + this.params = params; + } + } + + //~ Instance fields -------------------------------------------------------- + /** Name of the program */ + private final String toolName; + + /** The CLI arguments */ + private final String[] args; + + /** The parameters to fill */ + private final Parameters parameters; + + //~ Constructors ----------------------------------------------------------- + //-----// + // CLI // + //-----// + /** + * Creates a new CLI object. + * + * @param toolName the program name + * @param args the CLI arguments + */ + public CLI (final String toolName, + final String... args) + { + this.toolName = toolName; + this.args = Arrays.copyOf(args, args.length); + logger.debug("CLI args: {}", Arrays.toString(args)); + + parameters = parse(); + + if (parameters != null) { + parameters.setImpliedSteps(); + } + } + + //~ Methods ---------------------------------------------------------------- + //---------------// + // getParameters // + //---------------// + /** + * Parse the CLI arguments and return the populated parameters + * structure. + * + * @return the parsed parameters, or null if failed + */ + public Parameters getParameters () + { + return parameters; + } + + //----------// + // toString // + //----------// + @Override + public String toString () + { + StringBuilder sb = new StringBuilder(); + + for (String arg : args) { + sb.append(" ").append(arg); + } + + return sb.toString(); + } + + //---------// + // addItem // + //---------// + /** + * Add an item to a provided list, while handling indirections if + * needed. + * + * @param item the item to add, which can be a plain string (which is + * simply added to the list) or an indirection + * (a string starting by the '@' character) + * which denotes a file of items to be recursively added + * @param list the collection of items to be augmented + */ + private void addItem (String item, + List list) + { + // The item may be a plain string or the name of a pack that lists + // item(s). This is signalled by a starting '@' character in string + if (item.startsWith("@")) { + // File with other items inside + String pack = item.substring(1); + BufferedReader br = null; + + try { + br = new BufferedReader(new FileReader(pack)); + + String newRef; + + try { + while ((newRef = br.readLine()) != null) { + addItem(newRef.trim(), list); + } + + br.close(); + } catch (IOException ex) { + logger.warn("IO error while reading file ''{}''", pack); + } + } catch (FileNotFoundException ex) { + logger.warn("Cannot find file ''{}''", pack); + } finally { + if (br != null) { + try { + br.close(); + } catch (Exception ignored) { + } + } + } + } else if (item.length() > 0) { + // Plain item + list.add(item); + } + } + + //-----------------// + // decodeConstants // + //-----------------// + /** + * Retrieve properties out of the flat sequence of "key = value" + * pairs. + * + * @param constantPairs the flat sequence of key = value pairs + * @return the resulting constant properties + */ + private Properties decodeConstants (List constantPairs) + throws IOException + { + Properties props = new Properties(); + + // Use a simple string buffer in memory + StringBuilder sb = new StringBuilder(); + + for (String pair : constantPairs) { + sb.append(pair).append("\n"); + } + + props.load(new StringReader(sb.toString())); + + return props; + } + + //-------// + // parse // + //-------// + /** + * Parse the CLI arguments and populate the parameters structure. + * + * @return the populated parameters structure, or null if failed + */ + private Parameters parse () + { + // Status of the finite state machine + boolean paramNeeded = false; // Are we expecting a param? + boolean paramForbidden = false; // Are we not expecting a param? + Command command = Command.INPUT; // By default + Parameters params = new Parameters(); + List optionPairs = new ArrayList<>(); + List stepStrings = new ArrayList<>(); + List pageStrings = new ArrayList<>(); + + // Parse all arguments from command line + for (int i = 0; i < args.length; i++) { + String token = args[i]; + + if (token.startsWith("-")) { + // This is a new command + // Check that we were not expecting param(s) + if (paramNeeded) { + printCommandLine(); + stopUsage( + "Found no parameter after command '" + command + "'"); + + return null; + } + + // Remove leading minus sign and switch to uppercase + // To recognize command + token = token.substring(1).toUpperCase(Locale.ENGLISH); + + boolean found = false; + + for (Command cmd : Command.values()) { + if (token.equals(cmd.name())) { + command = cmd; + paramNeeded = command.card != Card.NONE; + paramForbidden = !paramNeeded; + found = true; + + break; + } + } + + // No command recognized + if (!found) { + printCommandLine(); + stopUsage("Unknown command '-" + token + "'"); + + return null; + } + + // Commands with no parameters + switch (command) { + case HELP: { + stopUsage(null); + + return null; + } + + case BATCH: + params.batchMode = true; + + break; + } + } else { + // This is a parameter for the current command + // Check we can accept a parameter + if (paramForbidden) { + printCommandLine(); + stopUsage( + "Extra parameter '" + token + + "' found after command '" + command + "'"); + + return null; + } + + switch (command) { + case STEP: + addItem(token, stepStrings); + + break; + + case OPTION: + addItem(token, optionPairs); + + break; + + case SCRIPT: + addItem(token, params.scriptNames); + + break; + + case INPUT: + addItem(token, params.inputNames); + + break; + + case PAGES: + addItem(token, pageStrings); + + break; + + case BENCH: + params.benchPath = token; + + break; + + case EXPORT: + params.exportPath = token; + + break; + + // case MIDI : + // params.midiPath = token; + // + // break; + case PRINT: + params.printPath = token; + + break; + + default: + } + + paramNeeded = false; + paramForbidden = command.card == Card.SINGLE; + } + } + + // Additional error checking + if (paramNeeded) { + printCommandLine(); + stopUsage("Expecting a token after command '" + command + "'"); + + return null; + } + + // Decode option pairs + try { + params.options = decodeConstants(optionPairs); + } catch (Exception ex) { + logger.warn("Error decoding -option ", ex); + } + + // Check step names + for (String stepString : stepStrings) { + try { + // Read a step name + params.desiredSteps.add( + Steps.valueOf(stepString.toUpperCase())); + } catch (Exception ex) { + printCommandLine(); + stopUsage( + "Step name expected, found '" + stepString + "' instead"); + + return null; + } + } + + // Check page ids + for (String pageString : pageStrings) { + try { + // Read a page id (counted from 1) + int id = Integer.parseInt(pageString); + if (params.pages == null) { + params.pages = new TreeSet<>(); + } + params.pages.add(id); + } catch (Exception ex) { + printCommandLine(); + stopUsage( + "Page id expected, found '" + pageString + "' instead"); + + return null; + } + } + + // Results + logger.debug(Main.dumping.dumpOf(params)); + + return params; + } + + //------------------// + // printCommandLine // + //------------------// + /** + * Printout the command line with its actual parameters. + */ + private void printCommandLine () + { + StringBuilder sb = new StringBuilder("Command line parameters: "); + + if (toolName != null) { + sb.append(toolName).append(" "); + } + + sb.append(this); + logger.info(sb.toString()); + } + + //-----------// + // stopUsage // + //-----------// + /** + * Printout a message if any, followed by the general syntax for + * the command line. + * + * @param msg the message to print if non null + */ + private void stopUsage (String msg) + { + // Print message if any + if (msg != null) { + logger.warn(msg); + } + + StringBuilder buf = new StringBuilder(); + + // Print version + buf.append("\nVersion:"); + buf.append("\n ").append(WellKnowns.TOOL_REF); + + // Print arguments syntax + buf.append("\nArguments syntax:"); + + for (Command command : Command.values()) { + buf.append( + String.format( + "%n %-36s %s", + String.format( + " [-%s%s]", + command.toString().toLowerCase(Locale.ENGLISH), + ((command.params != null) ? (" " + command.params) : "")), + command.description)); + } + + // Print all allowed step names + buf.append("\n\nKnown step names are in order").append( + " (non case-sensitive):"); + + for (Step step : Steps.values()) { + buf.append( + String.format( + "%n %-11s : %s", + step.toString().toUpperCase(), + step.getDescription())); + } + + logger.info(buf.toString()); + } + + //~ Inner Classes ---------------------------------------------------------- + //------------// + // Parameters // + //------------// + /** + * A structure that collects the various parameters parsed out of + * the command line. + */ + public static class Parameters + { + //~ Instance fields ---------------------------------------------------- + + /** Flag that indicates a batch mode */ + boolean batchMode = false; + + /** The set of desired steps */ + final Set desiredSteps = new LinkedHashSet<>(); + + /** The map of options */ + Properties options = null; + + /** The list of script file names to execute */ + final List scriptNames = new ArrayList<>(); + + /** The list of input image file names to load */ + final List inputNames = new ArrayList<>(); + + /** The set of page ids to load */ + SortedSet pages = null; + + /** Where log data is to be saved */ + ///String logPath = null; + /** Where bench data is to be saved */ + String benchPath = null; + + /** Where exported score data (MusicXML) is to be saved */ + String exportPath = null; + + /** Where MIDI data is to be saved */ + String midiPath = null; + + /** Where printed score (PDF) is to be saved */ + String printPath = null; + + //~ Constructors ------------------------------------------------------- + private Parameters () + { + } + + //~ Methods ------------------------------------------------------------ + //-----------------// + // setImpliedSteps // + //-----------------// + /** + * Some output parameters require their related step to be set. + */ + private void setImpliedSteps () + { + if (exportPath != null) { + desiredSteps.add(Steps.valueOf(Steps.EXPORT)); + } + + if (printPath != null) { + desiredSteps.add(Steps.valueOf(Steps.PRINT)); + } + + // if (midiPath != null) { + // desiredSteps.add(Steps.valueOf(Steps.MIDI)); + // } + } + } +} diff --git a/src/main/omr/Debug.java b/src/main/omr/Debug.java new file mode 100644 index 0000000..44aa788 --- /dev/null +++ b/src/main/omr/Debug.java @@ -0,0 +1,173 @@ +//----------------------------------------------------------------------------// +// // +// D e b u g // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr; + +import omr.glyph.GlyphRepository; +import omr.glyph.Shape; +import omr.glyph.ShapeDescription; +import omr.glyph.ShapeSet; +import omr.glyph.facets.Glyph; + +import omr.score.ui.ScoreDependent; + +import org.jdesktop.application.Action; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.awt.event.ActionEvent; +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileOutputStream; +import java.io.OutputStreamWriter; +import java.io.PrintWriter; +import java.util.List; + +/** + * Convenient class meant to temporarily inject some debugging. + * To be used in sync with file user-actions.xml in config folder + * + * @author Hervé Bitteur + */ +public class Debug + extends ScoreDependent +{ + //~ Static fields/initializers --------------------------------------------- + + /** Usual logger utility */ + private static final Logger logger = LoggerFactory.getLogger(Debug.class); + + //~ Methods ---------------------------------------------------------------- + // //------------------// + // // injectChordNames // + // //------------------// + // @Action(enabledProperty = SHEET_AVAILABLE) + // public void injectChordNames (ActionEvent e) + // { + // Score score = ScoreController.getCurrentScore(); + // + // if (score == null) { + // return; + // } + // + // ScoreSystem system = score.getFirstPage() + // .getFirstSystem(); + // system.acceptChildren(new ChordInjector()); + // } + // //---------------// + // // ChordInjector // + // //---------------// + // private static class ChordInjector + // extends AbstractScoreVisitor + // { + // //~ Static fields/initializers ----------------------------------------- + // + // /** List of symbols to inject. */ + // private static final String[] shelf = new String[] { + // "BMaj7/D#", "BMaj7", "G#m9", + // "F#", "C#7sus4", "F#" + // }; + // + // //~ Instance fields ---------------------------------------------------- + // + // /** Current index to symbol to inject. */ + // private int symbolCount = 0; + // + // //~ Methods ------------------------------------------------------------ + // + // @Override + // public boolean visit (ChordSymbol symbol) + // { + // // Replace chord info by one taken from the shelf + // if (symbolCount < shelf.length) { + // symbol.info = ChordInfo.create(shelf[symbolCount++]); + // } + // + // return false; + // } + // } + //------------------// + // saveTrainingData // + //------------------// + @Action + public void saveTrainingData (ActionEvent e) + { + final PrintWriter out = getPrintWriter(new File("glyphs.arff")); + + out.println("@relation " + "glyphs"); + out.println(); + + for (String label : ShapeDescription.getParameterLabels()) { + out.println("@attribute " + label + " real"); + } + + // Last attribute: shape + out.print("@attribute shape {"); + + for (Shape shape : ShapeSet.allPhysicalShapes) { + out.print(shape); + + if (shape != Shape.LAST_PHYSICAL_SHAPE) { + out.print(", "); + } + } + + out.println("}"); + + out.println(); + out.println("@data"); + + GlyphRepository repository = GlyphRepository.getInstance(); + List gNames = repository.getWholeBase(null); + logger.info("Glyphs: {}", gNames.size()); + + for (String gName : gNames) { + Glyph glyph = repository.getGlyph(gName, null); + + if (glyph != null) { + double[] ins = ShapeDescription.features(glyph); + + for (double in : ins) { + out.print((float) in); + out.print(","); + } + + out.println(glyph.getShape().getPhysicalShape()); + + //break; ///////////////////////////////////////////////////////// + } + } + + out.flush(); + out.close(); + logger.info("Done."); + } + + //----------------// + // getPrintWriter // + //----------------// + private static PrintWriter getPrintWriter (File file) + { + try { + final BufferedWriter bw = new BufferedWriter( + new OutputStreamWriter( + new FileOutputStream(file), + WellKnowns.FILE_ENCODING)); + + return new PrintWriter(bw); + } catch (Exception ex) { + System.err.println("Error creating " + file + ex); + + return null; + } + } +} diff --git a/src/main/omr/Main.java b/src/main/omr/Main.java new file mode 100644 index 0000000..b1142ee --- /dev/null +++ b/src/main/omr/Main.java @@ -0,0 +1,548 @@ +//----------------------------------------------------------------------------// +// // +// M a i n // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr; + +import omr.constant.Constant; +import omr.constant.ConstantManager; +import omr.constant.ConstantSet; + +import omr.score.Score; + +import omr.script.ScriptManager; + +import omr.step.ProcessingCancellationException; +import omr.step.Stepping; + +import omr.ui.MainGui; +import omr.ui.symbol.MusicFont; + +import omr.util.ClassUtil; +import omr.util.Clock; +import omr.util.Dumping; +import omr.util.OmrExecutors; + +import org.jdesktop.application.Application; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.Properties; +import java.util.SortedSet; +import java.util.concurrent.Callable; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; + +/** + * Class {@code Main} is the main class for OMR application. + * It deals with the main routine and its command line parameters. + * It launches the User Interface, unless a batch mode is selected. + * + * @see CLI + * + * @author Hervé Bitteur + */ +public class Main +{ + //~ Static fields/initializers --------------------------------------------- + + static { + /** Time stamp */ + Clock.resetTime(); + } + + /** Master View */ + private static MainGui gui; + + /** Usual logger utility */ + private static final Logger logger = LoggerFactory.getLogger(Main.class); + + /** Specific application parameters */ + private static final Constants constants = new Constants(); + + /** Parameters read from CLI */ + private static CLI.Parameters parameters; + + /** The application dumping service */ + public static final Dumping dumping = new Dumping(Main.class.getPackage()); + + //~ Constructors ----------------------------------------------------------- + //------// + // Main // + //------// + private Main () + { + } + + //~ Methods ---------------------------------------------------------------- + //--------// + // doMain // + //--------// + /** + * Specific starting method for the application. + * + * @param args command line parameters + * @see omr.CLI the possible command line parameters + */ + public static void doMain (String[] args) + { + // Initialize tool parameters + initialize(); + + // Process CLI arguments + process(args); + + // Locale to be used in the whole application? + checkLocale(); + + // Environment + showEnvironment(); + + // Native libs + loadNativeLibraries(); + + if (!parameters.batchMode) { + // For interactive mode + logger.debug("Main. Launching MainGui"); + Application.launch(MainGui.class, args); + } else { + // For batch mode + + // Remember if at least one task failed + boolean failure = false; + + // Check MusicFont is loaded + MusicFont.checkMusicFont(); + + // Launch the required tasks, if any + List> tasks = new ArrayList>(); + tasks.addAll(getFilesTasks()); + tasks.addAll(getScriptsTasks()); + + if (!tasks.isEmpty()) { + try { + logger.info("Submitting {} task(s)", tasks.size()); + + List> futures = OmrExecutors.getCachedLowExecutor() + .invokeAll( + tasks, + constants.processTimeOut.getValue(), + TimeUnit.SECONDS); + logger.info("Checking {} task(s)", tasks.size()); + + // Check for time-out + for (Future future : futures) { + try { + future.get(); + } catch (Exception ex) { + logger.warn("Future exception", ex); + failure = true; + } + } + } catch (Exception ex) { + logger.warn("Error in processing tasks", ex); + failure = true; + } + } + + // At this point all tasks have completed (normally or not) + // So shutdown immediately the executors + OmrExecutors.shutdown(true); + + // Store latest constant values on disk? + if (constants.persistBatchCliConstants.getValue()) { + ConstantManager.getInstance() + .storeResource(); + } + + // Stop the JVM with failure status? + if (failure) { + logger.warn("Exit with failure status"); + System.exit(-1); + } + } + } + + //--------------// + // getBenchPath // + //--------------// + /** + * Report the bench path if present on the CLI + * + * @return the CLI bench path, or null + */ + public static String getBenchPath () + { + return parameters.benchPath; + } + + //-----------------// + // getCliConstants // + //-----------------// + /** + * Report the properties set at the CLI level + * + * @return the CLI-defined constant values + */ + public static Properties getCliConstants () + { + if (parameters == null) { + return null; + } else { + return parameters.options; + } + } + + //---------------// + // getExportPath // + //---------------// + /** + * Report the export path if present on the CLI + * + * @return the CLI export path, or null + */ + public static String getExportPath () + { + return parameters.exportPath; + } + + //---------------// + // getFilesTasks // + //---------------// + /** + * Prepare the processing of image files listed on command line + * + * @return the collection of proper callables + */ + public static List> getFilesTasks () + { + List> tasks = new ArrayList>(); + + // Launch desired step on each score in parallel + for (final String name : parameters.inputNames) { + final File file = new File(name); + + tasks.add( + new Callable() + { + @Override + public Void call () + throws Exception + { + if (!parameters.desiredSteps.isEmpty()) { + logger.info( + "Launching {} on {} {}", + parameters.desiredSteps, + name, + (parameters.pages != null) + ? ("pages " + + parameters.pages) + : ""); + } + + if (file.exists()) { + final Score score = new Score(file); + + try { + Stepping.processScore( + parameters.desiredSteps, + parameters.pages, + score); + } catch (ProcessingCancellationException pce) { + logger.warn("Cancelled " + score, pce); + score.getBench() + .recordCancellation(); + throw pce; + } catch (Throwable ex) { + logger.warn("Exception occurred", ex); + throw ex; + } finally { + // Close (when in batch mode only) + if (gui == null) { + score.close(); + } + + return null; + } + } else { + String msg = "Could not find file " + + file.getCanonicalPath(); + logger.warn(msg); + throw new RuntimeException(msg); + } + } + }); + } + + return tasks; + } + + //--------// + // getGui // + //--------// + /** + * Points to the single instance of the User Interface, if any. + * + * @return MainGui instance, which may be null + */ + public static MainGui getGui () + { + return gui; + } + + //-------------// + // getMidiPath // + //-------------// + /** + * Report the midi path if present on the CLI + * + * @return the CLI midi path, or null + */ + public static String getMidiPath () + { + return parameters.midiPath; + } + + //-------------// + // getPagesIds // + //-------------// + /** + * Report the set of page ids if present on the CLI + * + * @return the CLI page ids, or null + */ + public static SortedSet getPageIds () + { + return parameters.pages; + } + + //--------------// + // getPrintPath // + //--------------// + /** + * Report the print path if present on the CLI + * + * @return the CLI print path, or null + */ + public static String getPrintPath () + { + return parameters.printPath; + } + + //-----------------// + // getScriptsTasks // + //-----------------// + /** + * Prepare the processing of scripts listed on command line + * + * @return the collection of proper script callables + */ + public static List> getScriptsTasks () + { + List> tasks = new ArrayList>(); + + // Launch desired scripts in parallel + for (String name : parameters.scriptNames) { + final String scriptName = name; + + tasks.add( + new Callable() + { + @Override + public Void call () + throws Exception + { + ScriptManager.getInstance() + .loadAndRun(new File(scriptName)); + + return null; + } + }); + } + + return tasks; + } + + //--------// + // setGui // + //--------// + /** + * Register the GUI (done by the GUI itself when it is ready) + * + * @param gui the MainGui instance + */ + public static void setGui (MainGui gui) + { + Main.gui = gui; + } + + //-------------// + // checkLocale // + //-------------// + private static void checkLocale () + { + final String localeStr = constants.locale.getValue() + .trim(); + + if (!localeStr.isEmpty()) { + for (Locale locale : Locale.getAvailableLocales()) { + if (locale.toString() + .equalsIgnoreCase(localeStr)) { + Locale.setDefault(locale); + logger.debug("Locale set to {}", locale); + + return; + } + } + + logger.warn("Cannot set locale to {}", localeStr); + } + } + + //------------// + // initialize // + //------------// + private static void initialize () + { + // (re) Open the executor services + OmrExecutors.restart(); + } + + //---------------------// + // loadNativeLibraries // + //---------------------// + /** + * Explicitly load all the needed native libraries. + */ + private static void loadNativeLibraries () + { + // Explicitly load all native libs resources and in proper order + logger.info("Loading native libraries ..."); + + boolean success = true; + + if (WellKnowns.WINDOWS) { + // For Windows, drop only the ".dll" suffix + success &= ClassUtil.loadLibrary("jniTessBridge"); + success &= ClassUtil.loadLibrary("libtesseract302"); + success &= ClassUtil.loadLibrary("liblept168"); + } else if (WellKnowns.LINUX) { + // For Linux, drop both the "lib" prefix and the ".so" suffix + success &= ClassUtil.loadLibrary("jniTessBridge"); + } + + if (success) { + logger.info("All libraries loaded."); + } else { + // Inform user of OCR installation problem + String msg = "Tesseract OCR is not installed properly"; + + if (Main.getGui() != null) { + Main.getGui() + .displayError(msg); + } else { + logger.warn(msg); + } + } + } + + //---------// + // process // + //---------// + private static void process (String[] args) + { + // First get the provided arguments if any + parameters = new CLI(WellKnowns.TOOL_NAME, args).getParameters(); + + if (parameters == null) { + logger.warn("Exiting ..."); + + // Stop the JVM, with failure status (1) + Runtime.getRuntime() + .exit(1); + } + + // Interactive or Batch mode ? + if (parameters.batchMode) { + logger.info("Running in batch mode"); + + ///System.setProperty("java.awt.headless", "true"); + + // // Check MIDI output is not asked for + // Step midiStep = Steps.valueOf(Steps.MIDI); + // + // if ((midiStep != null) && + // parameters.desiredSteps.contains(midiStep)) { + // logger.warn( + // "MIDI output is not compatible with -batch mode." + + // " MIDI output is ignored."); + // parameters.desiredSteps.remove(midiStep); + // } + } else { + logger.debug("Running in interactive mode"); + } + } + + //-----------------// + // showEnvironment // + //-----------------// + /** + * Show the application environment to the user. + */ + private static void showEnvironment () + { + if (constants.showEnvironment.isSet()) { + logger.info( + "Environment:\n" + "- Audiveris: {}\n" + + "- OS: {}\n" + "- Architecture: {}\n" + + "- Java VM: {}", + WellKnowns.TOOL_REF + ":" + WellKnowns.TOOL_BUILD, + System.getProperty("os.name") + " " + + System.getProperty("os.version"), + System.getProperty("os.arch"), + System.getProperty("java.vm.name") + " (build " + + System.getProperty("java.vm.version") + ", " + + System.getProperty("java.vm.info") + ")"); + } + } + + //~ Inner Classes ---------------------------------------------------------- + //-----------// + // Constants // + //-----------// + private static final class Constants + extends ConstantSet + { + //~ Instance fields ---------------------------------------------------- + + private final Constant.Boolean showEnvironment = new Constant.Boolean( + true, + "Should we show environment?"); + + private final Constant.String locale = new Constant.String( + "en", + "Locale language to be used in the whole application (en, fr)"); + + private final Constant.Boolean persistBatchCliConstants = new Constant.Boolean( + false, + "Should we persist CLI-defined constants when running in batch?"); + + private final Constant.Integer processTimeOut = new Constant.Integer( + "Seconds", + 300, + "Process time-out, specified in seconds"); + + } +} diff --git a/src/main/omr/WellKnowns.java b/src/main/omr/WellKnowns.java new file mode 100644 index 0000000..0acdb1d --- /dev/null +++ b/src/main/omr/WellKnowns.java @@ -0,0 +1,501 @@ +//----------------------------------------------------------------------------// +// // +// W e l l K n o w n s // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr; + +import omr.log.LogUtil; +import static omr.util.UriUtil.*; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.lang.reflect.Field; +import java.net.JarURLConnection; +import java.net.URI; +import java.net.URL; +import java.nio.file.Paths; +import java.nio.file.attribute.FileTime; +import java.util.Enumeration; +import java.util.Locale; +import java.util.jar.JarFile; + +/** + * Class {@code WellKnowns} gathers top public static final data to be + * shared within Audiveris application. + * + *

Note that a few initial operations are performed here, because they need + * to take place before any other class is loaded. + * + * @author Hervé Bitteur + */ +public class WellKnowns +{ + //~ Static fields/initializers --------------------------------------------- + + //----------// + // IDENTITY // + //----------// + // + /** Application company name: {@value}. */ + public static final String COMPANY_NAME = ProgramId.COMPANY_NAME; + + /** Application company id: {@value}. */ + public static final String COMPANY_ID = ProgramId.COMPANY_ID; + + /** Application name: {@value}. */ + public static final String TOOL_NAME = ProgramId.NAME; + + /** Application reference: {@value}. */ + public static final String TOOL_REF = ProgramId.VERSION + "." + + ProgramId.REVISION; + + /** Application build: {@value}. */ + public static final String TOOL_BUILD = ProgramId.BUILD; + + /** Specific prefix for application folders: {@value} */ + private static final String TOOL_PREFIX = "/" + COMPANY_ID + "/" + + TOOL_NAME; + + //----------// + // PLATFORM // + //----------// + // + /** Name of operating system */ + private static final String OS_NAME = System.getProperty("os.name") + .toLowerCase(Locale.ENGLISH); + + /** Are we using a Linux OS?. */ + public static final boolean LINUX = OS_NAME.startsWith("linux"); + + /** Are we using a Mac OS?. */ + public static final boolean MAC_OS_X = OS_NAME.startsWith("mac os x"); + + /** Are we using a Windows OS?. */ + public static final boolean WINDOWS = OS_NAME.startsWith("windows"); + + /** Precise OS architecture. */ + public static final String OS_ARCH = System.getProperty("os.arch"); + + /** Are we using Windows on 64 bit architecture?. */ + public static final boolean WINDOWS_64 = WINDOWS + && (System.getenv( + "ProgramFiles(x86)") != null); + + /** File character encoding. */ + public static final String FILE_ENCODING = getFileEncoding(); + + /** File separator for the current platform. */ + public static final String FILE_SEPARATOR = System.getProperty( + "file.separator"); + + /** Line separator for the current platform. */ + public static final String LINE_SEPARATOR = System.getProperty( + "line.separator"); + + /** Redirection, if any, of standard out and err streams. */ + public static final String STD_OUT_ERR = System.getProperty("stdouterr"); + + //---------// + // PROGRAM // This is a read-only area + //---------// + // + /** The container from which the application classes were loaded. */ + public static final URI CLASS_CONTAINER = getClassContainer(); + + /** + * Running from normal jar archive rather than class files? . + * When running directly from .class files, we are in development mode and + * want to have very short cycles, data is retrieved from local files. + * When running from .jar archive, we are in standard mode whereby most data + * is meant to be retrieved from the .jar archive itself. + */ + public static final boolean RUNNING_FROM_JAR = runningFromJar(); + + /** Containing jar file, if any. */ + public static final JarFile JAR_FILE = RUNNING_FROM_JAR ? getJarFile() : null; + + /** Time of last modification of jar file, if any. */ + public static final FileTime JAR_TIME = RUNNING_FROM_JAR ? getJarTime() : null; + + /** The uri where resource data is stored. */ + public static final URI RES_URI = RUNNING_FROM_JAR + ? toURI( + WellKnowns.class.getClassLoader().getResource("res")) + : Paths.get("res") + .toUri(); + + /** The folder where Tesseract OCR material is stored. */ + public static final File OCR_FOLDER = getOcrFolder(); + + //-------------// read-write area + // USER CONFIG // Configuration files the user can edit on his own + //-------------// + // + /** The config folder where global configuration data is stored. */ + public static final File CONFIG_FOLDER = RUNNING_FROM_JAR + ? getConfigFolder() + : new File("config"); + + /** The folder where plugin scripts are found. */ + public static final File PLUGINS_FOLDER = new File( + CONFIG_FOLDER, + "plugins"); + + //-----------// read-write area + // USER DATA // User-specific data, except configuration stuff + //-----------// + // + /** Base folder for data */ + public static final File DATA_FOLDER = RUNNING_FROM_JAR ? getDataFolder() + : new File("data"); + + /** + * The folder where documentations files are installed. + * Installation takes place when .jar is run for the first time + */ + public static final File DOC_FOLDER = new File(DATA_FOLDER, "www"); + + /** + * The folder where examples files are installed. + * Installation takes place when .jar is run for the first time + */ + public static final File EXAMPLES_FOLDER = new File( + DATA_FOLDER, + "examples"); + + /** The folder where temporary data can be stored. */ + public static final File TEMP_FOLDER = new File(DATA_FOLDER, "temp"); + + /** The folder where evaluation data is stored. */ + public static final File EVAL_FOLDER = new File(DATA_FOLDER, "eval"); + + /** The folder where training material is stored. */ + public static final File TRAIN_FOLDER = new File(DATA_FOLDER, "train"); + + /** The folder where symbols information is stored. */ + public static final File SYMBOLS_FOLDER = new File(TRAIN_FOLDER, "symbols"); + + /** The default folder where benches data is stored. */ + public static final File DEFAULT_BENCHES_FOLDER = new File( + DATA_FOLDER, + "benches"); + + /** The default folder where PDF data is stored. */ + public static final File DEFAULT_PRINT_FOLDER = new File( + DATA_FOLDER, + "print"); + + /** The default folder where scripts data is stored. */ + public static final File DEFAULT_SCRIPTS_FOLDER = new File( + DATA_FOLDER, + "scripts"); + + /** The default folder where scores data is stored. */ + public static final File DEFAULT_SCORES_FOLDER = new File( + DATA_FOLDER, + "scores"); + + static { + /** Logging configuration. */ + LogUtil.initialize(CONFIG_FOLDER, TEMP_FOLDER); + /** Log declared data (debug). */ + logDeclaredData(); + } + + static { + /** Disable DirecDraw by default. */ + disableDirectDraw(); + } + + //~ Constructors ----------------------------------------------------------- + // + //------------// + // WellKnowns // Not meant to be instantiated + //------------// + private WellKnowns () + { + } + + //~ Methods ---------------------------------------------------------------- + //--------------// + // ensureLoaded // + //--------------// + /** + * Make sure this class is loaded. + */ + public static void ensureLoaded () + { + } + + // + //-------------------// + // disableDirectDraw // + //-------------------// + private static void disableDirectDraw () + { + // See http://performance.netbeans.org/howto/jvmswitches/ + // -Dsun.java2d.d3d=false + // this switch disables DirectDraw and may solve performance problems + // with some HW configurations. + final String KEY = "sun.java2d.d3d"; + + // Respect user setting if any + if (System.getProperty(KEY) == null) { + System.setProperty(KEY, "false"); + } + } + + //-------------------// + // getClassContainer // + //-------------------// + private static URI getClassContainer () + { + return toURI( + WellKnowns.class.getProtectionDomain().getCodeSource().getLocation()); + } + + //-----------------// + // getConfigFolder // + //-----------------// + private static File getConfigFolder () + { + if (WINDOWS) { + String appdata = System.getenv("APPDATA"); + + if (appdata != null) { + return new File(appdata + TOOL_PREFIX + "/config"); + } + + throw new RuntimeException( + "APPDATA environment variable is not set"); + } else if (MAC_OS_X) { + String config = System.getenv("XDG_CONFIG_HOME"); + + if (config != null) { + return new File(config + System.getProperty("user.dir")); + } + + String home = System.getenv("HOME"); + + if (home != null) { + return new File(System.getProperty("user.dir")); + } + + throw new RuntimeException("HOME environment variable is not set"); + } else if (LINUX) { + String config = System.getenv("XDG_CONFIG_HOME"); + + if (config != null) { + return new File(config + TOOL_PREFIX); + } + + String home = System.getenv("HOME"); + + if (home != null) { + return new File(home + "/.config" + TOOL_PREFIX); + } + + throw new RuntimeException("HOME environment variable is not set"); + } else { + throw new RuntimeException("Platform unknown"); + } + } + + //---------------// + // getDataFolder // + //---------------// + private static File getDataFolder () + { + if (WINDOWS) { + String appdata = System.getenv("APPDATA"); + + if (appdata != null) { + return new File(appdata + TOOL_PREFIX + "/data"); + } + + throw new RuntimeException( + "APPDATA environment variable is not set"); + } else if (MAC_OS_X) { + String data = System.getenv("XDG_DATA_HOME"); + + if (data != null) { + return new File(System.getProperty("user.dir")); + } + + String home = System.getenv("HOME"); + + if (home != null) { + return new File(System.getProperty("user.dir")); + } + + throw new RuntimeException("HOME environment variable is not set"); + } else if (LINUX) { + String data = System.getenv("XDG_DATA_HOME"); + + if (data != null) { + return new File(data + TOOL_PREFIX); + } + + String home = System.getenv("HOME"); + + if (home != null) { + return new File(home + "/.local/share" + TOOL_PREFIX); + } + + throw new RuntimeException("HOME environment variable is not set"); + } else { + throw new RuntimeException("Platform unknown"); + } + } + + //-----------------// + // getFileEncoding // + //-----------------// + private static String getFileEncoding () + { + final String ENCODING_KEY = "file.encoding"; + final String ENCODING_VALUE = "UTF-8"; + + System.setProperty(ENCODING_KEY, ENCODING_VALUE); + + return ENCODING_VALUE; + } + + //------------// + // getJarFile // + //------------// + private static JarFile getJarFile () + { + try { + String rn = WellKnowns.class.getName() + .replace('.', '/') + + ".class"; + Enumeration en = WellKnowns.class.getClassLoader() + .getResources(rn); + + if (en.hasMoreElements()) { + URL url = en.nextElement(); + + // url = jar:http://audiveris.kenai.com/jnlp/audiveris.jar!/omr/WellKnowns.class + JarURLConnection urlcon = (JarURLConnection) (url.openConnection()); + JarFile jarFile = urlcon.getJarFile(); + + return jarFile; + } + } catch (Exception ex) { + System.out.print("Error getting jar file " + ex); + } + + return null; + } + + //------------// + // getJarTime // + //------------// + private static FileTime getJarTime () + { + long millis = JAR_FILE.getEntry("META-INF/MANIFEST.MF") + .getTime(); + + return FileTime.fromMillis(millis); + } + + //--------------// + // getOcrFolder // + //--------------// + private static File getOcrFolder () + { + // First, try to use TESSDATA_PREFIX environment variable + // which might denote a Tesseract installation + final String TESSDATA_PREFIX = "TESSDATA_PREFIX"; + final String tessPrefix = System.getenv(TESSDATA_PREFIX); + + if (tessPrefix != null) { + File dir = new File(tessPrefix); + + if (dir.isDirectory()) { + return dir; + } + } + + // Fallback to default directory + if (LINUX) { + return new File("/usr/share/tesseract-ocr"); + } else if (WINDOWS) { + final String pf32 = OS_ARCH.equals("x86") ? "ProgramFiles" + : "ProgramFiles(x86)"; + + return new File(new File(System.getenv(pf32)), "tesseract-ocr"); + } else { + throw new InstallationException("Tesseract-OCR is not installed"); + } + } + + //-----------------// + // logDeclaredData // + //-----------------// + private static void logDeclaredData () + { + final Logger logger = LoggerFactory.getLogger(WellKnowns.class); + + // To check updates + ///logger.info("Token #{}", 2); + if (!RUNNING_FROM_JAR) { + // Just to remind the developer we are NOT running in normal mode + logger.info("[Not running from jar]"); + } else { + // Debug, to identify this jar + logger.debug( + "JarTime: {} JarFile: {}", + JAR_TIME, + JAR_FILE.getName()); + } + + if (logger.isDebugEnabled()) { + for (Field field : WellKnowns.class.getDeclaredFields()) { + try { + logger.debug("{}= {}", field.getName(), field.get(null)); + } catch (IllegalAccessException ex) { + ex.printStackTrace(); + } + } + } + } + + //----------------// + // runningFromJar // + //----------------// + private static boolean runningFromJar () + { + return CLASS_CONTAINER.toString() + .toLowerCase() + .endsWith(".jar"); + } + + //~ Inner Classes ---------------------------------------------------------- + //-----------------------// + // InstallationException // + //-----------------------// + /** + * Exception used to signal an installation error. + */ + public static class InstallationException + extends RuntimeException + { + //~ Constructors ------------------------------------------------------- + + public InstallationException (String message) + { + super(message); + } + } +} diff --git a/src/main/omr/action/ActionDescriptor.java b/src/main/omr/action/ActionDescriptor.java new file mode 100644 index 0000000..4ff63d7 --- /dev/null +++ b/src/main/omr/action/ActionDescriptor.java @@ -0,0 +1,115 @@ +//----------------------------------------------------------------------------// +// // +// A c t i o n D e s c r i p t o r // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.action; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlAttribute; +import javax.xml.bind.annotation.XmlRootElement; + +/** + * Class {@code ActionDescriptor} gathers parameters related to an action + * from the User Interface point of view + * + * @author Hervé Bitteur + */ +@XmlAccessorType(XmlAccessType.NONE) +@XmlRootElement(name = "action") +public class ActionDescriptor +{ + //~ Instance fields -------------------------------------------------------- + + /** Class name */ + @XmlAttribute(name = "class") + public String className; + + /** Name of method within class */ + @XmlAttribute(name = "method") + public String methodName; + + /** Which UI domain (menu) should host this action */ + @XmlAttribute(name = "domain") + public String domain; + + /** + * Which UI section should host this action. + * Any value is OK, but items with the same section value will be gathered + * together in the menu, while different sections will be separated by a + * menu separator + */ + @XmlAttribute(name = "section") + public Integer section; + + /** + * Which kind of menu item should be generated for this action, + * default is JMenuItem + */ + @XmlAttribute(name = "item") + public String itemClassName; + + /** + * Which kind of (toolbar) button should be generated for this action, + * default is null + */ + @XmlAttribute(name = "button") + public String buttonClassName; + + //~ Constructors ----------------------------------------------------------- + + //------------------// + // ActionDescriptor // + //------------------// + /** + * To force instantiation through JAXB unmarshalling only. + */ + private ActionDescriptor () + { + } + + //~ Methods ---------------------------------------------------------------- + + //----------// + // toString // + //----------// + /** + * Report a one-line information on this descriptor + */ + @Override + public String toString () + { + StringBuilder sb = new StringBuilder(); + sb.append("{action"); + sb.append(" class:"). + append(className). + append(" method:"). + append(methodName); + + sb.append(" domain:"). + append(domain); + sb.append(" section:"). + append(section); + + if (itemClassName != null) { + sb.append(" item:"). + append(itemClassName); + } + + if (buttonClassName != null) { + sb.append(" button:"). + append(buttonClassName); + } + + sb.append("}"); + + return sb.toString(); + } +} diff --git a/src/main/omr/action/ActionManager.java b/src/main/omr/action/ActionManager.java new file mode 100644 index 0000000..304639e --- /dev/null +++ b/src/main/omr/action/ActionManager.java @@ -0,0 +1,405 @@ +//----------------------------------------------------------------------------// +// // +// A c t i o n M a n a g e r // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.action; + +import omr.WellKnowns; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import omr.ui.MainGui; +import omr.ui.util.SeparableMenu; +import omr.ui.util.UIUtil; + +import omr.util.UriUtil; + +import org.jdesktop.application.ApplicationAction; +import org.jdesktop.application.ResourceMap; + +import java.io.File; +import java.io.InputStream; +import java.io.IOException; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.net.URI; +import java.net.URL; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; + +import javax.swing.AbstractButton; +import javax.swing.Action; +import javax.swing.ActionMap; +import javax.swing.Box; +import javax.swing.JMenu; +import javax.swing.JMenuBar; +import javax.swing.JMenuItem; +import javax.swing.JToolBar; +import javax.xml.bind.JAXBException; + +/** + * Class {@code ActionManager} handles the instantiation and dressing + * of actions, their organization in the menus and the tool bar, and + * their enabling. + * + * @author Hervé Bitteur + */ +public class ActionManager +{ + //~ Static fields/initializers --------------------------------------------- + + /** Usual logger utility */ + private static final Logger logger = LoggerFactory.getLogger(ActionManager.class); + + /** Class loader */ + private static final ClassLoader classLoader = ActionManager.class. + getClassLoader(); + + /** Singleton */ + private static volatile ActionManager INSTANCE; + + //~ Instance fields -------------------------------------------------------- + // + /** The map of all menus, so that we can directly provide some. */ + private final Map menuMap = new HashMap<>(); + + /** Collection of actions enabled only when a sheet is selected. */ + private final Collection sheetDependentActions = new ArrayList<>(); + + /** Collection of actions enabled only when current score is available. */ + private final Collection scoreDependentActions = new ArrayList<>(); + + /** The tool bar that hosts some actions. */ + private final JToolBar toolBar = new JToolBar(); + + /** The menu bar for all actions. */ + private final JMenuBar menuBar = new JMenuBar(); + + //~ Constructors ----------------------------------------------------------- + // + //---------------// + // ActionManager // + //---------------// + /** + * Meant to be instantiated at most once. + */ + private ActionManager () + { + } + + //~ Methods ---------------------------------------------------------------- + //-------------// + // getInstance // + //-------------// + /** + * Report the single action manager instance. + * + * @return the unique instance of this class + */ + public static ActionManager getInstance () + { + if (INSTANCE == null) { + INSTANCE = new ActionManager(); + } + + return INSTANCE; + } + + //-------------------// + // getActionInstance // + //-------------------// + /** + * Retrieve an action knowing its methodName. + * + * @param instance the instance of the hosting class + * @param methodName the method name + * @return the action found, or null if none + */ + public ApplicationAction getActionInstance (Object instance, + String methodName) + { + ActionMap actionMap = MainGui.getInstance().getContext().getActionMap( + instance); + + return (ApplicationAction) actionMap.get(methodName); + } + + //---------// + // getMenu // + //---------// + /** + * Report the menu built for a given key. + * + * @param key the given menu key + * @return the related menu + */ + public JMenu getMenu (String key) + { + return menuMap.get(key); + } + + //------------// + // getMenuBar // + //------------// + /** + * Report the bar containing all generated pull-down menus. + * + * @return the menu bar + */ + public JMenuBar getMenuBar () + { + return menuBar; + } + + //---------// + // getName // + //---------// + /** + * Report a describing name. + * + * @return a describing name + */ + public String getName () + { + return "ActionManager"; + } + + //------------// + // getToolBar // + //------------// + /** + * Report the tool bar containing all generated buttons. + * + * @return the tool bar + */ + public JToolBar getToolBar () + { + return toolBar; + } + + //------------// + // injectMenu // + //------------// + /** + * Insert a predefined menu, either partly or fully built. + * + * @param key the menu unique name + * @param menu the menu to inject + */ + public void injectMenu (String key, + JMenu menu) + { + menuMap.put(key, menu); + } + + //--------------------// + // loadAllDescriptors // + //--------------------// + /** + * Load all descriptors as found in system and user configuration + * files. + */ + public void loadAllDescriptors () + { + // Load classes first for system actions, then for user actions + URI[] uris = new URI[]{ + UriUtil.toURI(WellKnowns.RES_URI, "system-actions.xml"), + new File(WellKnowns.CONFIG_FOLDER, "user-actions.xml").toURI().normalize()}; + + for (int i = 0; i < uris.length; i++) { + URI uri = uris[i]; + try { + URL url = uri.toURL(); + InputStream input = url.openStream(); + Actions.loadActionsFrom(input); + } catch (IOException ex) { + // Item does not exist + if (i == 0) { + // Only the first item (system) is mandatory + logger.error("Mandatory file not found {}", uri); + } + } catch (JAXBException ex) { + logger.warn("Error loading actions from " + uri, ex); + } + } + } + + //--------------------// + // registerAllActions // + //--------------------// + /** + * Register all actions as listed in the descriptor files, and + * organize them according to the various domains defined. + * There is one pull-down menu generated for each domain found. + */ + public void registerAllActions () + { + // Insert an initial separator, to let user easily grab the toolBar + toolBar.addSeparator(); + + for (String domain : Actions.getDomainNames()) { + // Create dedicated menu for this range, if not already existing + JMenu menu = menuMap.get(domain); + + if (menu == null) { + logger.debug("Creating menu:{}", domain); + menu = new SeparableMenu(domain); + menuMap.put(domain, menu); + } else { + logger.debug("Augmenting menu:{}", domain); + } + + // Proper menu decoration + ResourceMap resource = MainGui.getInstance().getContext(). + getResourceMap(Actions.class); + menu.setText(domain); // As default + menu.setName(domain); + + // Register all actions in the given domain + registerDomainActions(domain, menu); + resource.injectComponents(menu); + + toolBar.addSeparator(); + + // Smart insertion of the menu into the menu bar + if (menu.getItemCount() > 0) { + if (domain.equalsIgnoreCase("help")) { + menuBar.add(Box.createHorizontalStrut(50)); + } + + SeparableMenu.trimSeparator(menu); // No separator at end + menuBar.add(menu); + } + } + } + + //----------------// + // registerAction // + //----------------// + /** + * Allocate and dress an instance of the provided class, then + * register the action in the UI structure (menus and buttons) + * according to the action descriptor parameters. + * + * @param action the provided action class + * @return the registered and decorated instance of the action class + */ + @SuppressWarnings("unchecked") + private ApplicationAction registerAction (ActionDescriptor desc) + { + ///logger.info("registerAction. " + desc); + ApplicationAction action = null; + + try { + // Retrieve proper class instance + Class classe = classLoader.loadClass(desc.className); + Object instance = null; + + // Reuse existing instance through a 'getInstance()' method if any + try { + Method getInstance = classe.getDeclaredMethod( + "getInstance", + (Class[]) null); + + if (Modifier.isStatic(getInstance.getModifiers())) { + instance = getInstance.invoke(null); + } + } catch (NoSuchMethodException ignored) { + } + + if (instance == null) { + // Fall back to allocate a new class instance + ///logger.warn("instantiating instance of " + classe); + instance = classe.newInstance(); + } + + // Retrieve the action instance + action = getActionInstance(instance, desc.methodName); + + if (action != null) { + // Insertion of a button on Tool Bar? + if (desc.buttonClassName != null) { + Class buttonClass = + (Class) classLoader. + loadClass(desc.buttonClassName); + AbstractButton button = buttonClass.newInstance(); + button.setAction(action); + toolBar.add(button); + button.setBorder(UIUtil.getToolBorder()); + button.setText(""); + } + } else { + logger.error("Unknown action {} in class {}", + desc.methodName, desc.className); + } + } catch (ClassNotFoundException | SecurityException | + IllegalAccessException | IllegalArgumentException | + InvocationTargetException | InstantiationException ex) { + logger.warn("Error while registering " + desc, ex); + } + + return action; + } + + //-----------------------// + // registerDomainActions // + //-----------------------// + @SuppressWarnings("unchecked") + private void registerDomainActions (String domain, + JMenu menu) + { + // Create all type sections for this menu + for (int section : Actions.getSections()) { + logger.debug("Starting section: {}", section); + + // Use a separator between sections + menu.addSeparator(); + + for (ActionDescriptor desc : Actions.getAllDescriptors()) { + if (desc.domain.equalsIgnoreCase(domain) + && (desc.section == section)) { + logger.debug("Registering {}", desc); + + try { + Class itemClass; + + if (desc.itemClassName != null) { + itemClass = (Class) classLoader. + loadClass( + desc.itemClassName); + } else { + itemClass = JMenuItem.class; + } + + JMenuItem item = itemClass.newInstance(); + item.setText(desc.methodName); + + ApplicationAction action = registerAction(desc); + + if (action != null) { + action.setSelected(action.isSelected()); + item.setAction(action); + menu.add(item); + } else { + logger.warn("Could not register {}", desc); + } + } catch (ClassNotFoundException | InstantiationException | + IllegalAccessException ex) { + logger.warn("Error with " + desc.itemClassName, ex); + } + } + } + } + } +} diff --git a/src/main/omr/action/Actions.java b/src/main/omr/action/Actions.java new file mode 100644 index 0000000..0928dcb --- /dev/null +++ b/src/main/omr/action/Actions.java @@ -0,0 +1,211 @@ +//----------------------------------------------------------------------------// +// // +// A c t i o n s // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.action; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.InputStream; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; +import java.util.SortedSet; +import java.util.TreeSet; + +import javax.xml.bind.JAXBContext; +import javax.xml.bind.JAXBException; +import javax.xml.bind.Unmarshaller; +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlRootElement; + +/** + * Class {@code Actions} handles all actions descriptors. + * + * @author Hervé Bitteur + */ +@XmlAccessorType(XmlAccessType.NONE) +@XmlRootElement(name = "actions") +public class Actions +{ + //~ Static fields/initializers --------------------------------------------- + + /** Usual logger utility */ + private static final Logger logger = LoggerFactory.getLogger(Actions.class); + + /** Context for JAXB unmarshalling */ + private static volatile JAXBContext jaxbContext; + + /** The collection of all actions loaded so far */ + private static final Set allDescriptors = new LinkedHashSet<>(); + + //~ Enumerations ----------------------------------------------------------- + /** + * Predefined list of domain names. + * Through the action list files, the user will be able to add new domain + * names. + * This classification is mainly used to define the related pull-down menus. + */ + public static enum Domain + { + //~ Enumeration constant initializers ---------------------------------- + + /** Domain of file actions */ + FILE, + /** Domain of individual steps */ + STEP, + /** Domain of score actions */ + SCORE, + /** Domain of MIDI features */ + MIDI, + /** Domain of various view features */ + VIEW, + /** Domain of utilities */ + TOOL, + /** Domain of plugins */ + PLUGIN, + /** Domain of help information */ + HELP; + } + + //~ Instance fields -------------------------------------------------------- + // + /** Collection of descriptors loaded by unmarshalling one file. */ + @XmlElement(name = "action") + private List descriptors = new ArrayList<>(); + + //~ Constructors ----------------------------------------------------------- + // + //---------// + // Actions // + //---------// + /** Not meant to be instantiated */ + private Actions () + { + } + + //~ Methods ---------------------------------------------------------------- + // + //-------------------// + // getAllDescriptors // + //-------------------// + /** + * Report the collection of descriptors loaded so far. + * @return all the loaded action descriptors + */ + public static Set getAllDescriptors () + { + return allDescriptors; + } + + //----------------// + // getDomainNames // + //----------------// + /** + * Report the whole collection of domain names, starting with the + * predefined ones. + * @return the collection of domain names + */ + public static Set getDomainNames () + { + Set names = new LinkedHashSet<>(); + + // Predefined ones, except HELP + for (Domain domain : Domain.values()) { + if (domain != Domain.HELP) { + names.add(domain.name()); + } + } + + // User-specified ones, except HELP + for (ActionDescriptor desc : allDescriptors) { + if (!desc.domain.equalsIgnoreCase(Domain.HELP.toString())) { + names.add(desc.domain); + } + } + + // Add HELP as the very last one + names.add(Domain.HELP.name()); + + return names; + } + + //-------------// + // getSections // + //-------------// + /** + * Report the whole collection of sections, the predefined ones + * and the added ones. + * @return the collection of sections + */ + public static SortedSet getSections () + { + SortedSet sections = new TreeSet<>(); + + for (ActionDescriptor desc : allDescriptors) { + sections.add(desc.section); + } + + return sections; + } + + //-----------------// + // loadActionsFrom // + //-----------------// + /** + * Unmarshal the provided XML stream to allocate the corresponding + * collection of action descriptors. + * + * @param in the input stream that contains the collection of action + * descriptors in XML format. The stream is not closed by this method + * @throws javax.xml.bind.JAXBException + */ + public static void loadActionsFrom (InputStream in) + throws JAXBException + { + if (jaxbContext == null) { + jaxbContext = JAXBContext.newInstance(Actions.class); + } + + Unmarshaller um = jaxbContext.createUnmarshaller(); + Actions actions = (Actions) um.unmarshal(in); + + for (ActionDescriptor desc : actions.descriptors) { + logger.debug("Descriptor unmarshalled {}", desc); + } + + // Verify that all actions have a domain and a section assigned + for (Iterator it = actions.descriptors.iterator(); + it.hasNext();) { + ActionDescriptor desc = it.next(); + + if (desc.domain == null) { + logger.warn("No domain specified for {}", desc); + it.remove(); + + continue; + } + + if (desc.section == null) { + logger.warn("No section specified for {}", desc); + it.remove(); + + continue; + } + } + + allDescriptors.addAll(actions.descriptors); + } +} diff --git a/src/main/omr/action/package.html b/src/main/omr/action/package.html new file mode 100644 index 0000000..e35ea01 --- /dev/null +++ b/src/main/omr/action/package.html @@ -0,0 +1,17 @@ + + + + + + Package omr.action + + + +

+ Package dedicated to the implementation of user actions, both + system (predefined) or user (custom) actions +

+ + + diff --git a/src/main/omr/action/resources/Actions.properties b/src/main/omr/action/resources/Actions.properties new file mode 100644 index 0000000..784a946 --- /dev/null +++ b/src/main/omr/action/resources/Actions.properties @@ -0,0 +1,36 @@ +# ---------------------------------------------------------------------------- # +# # +# A c t i o n s . p r o p e r t i e s # +# # +# ---------------------------------------------------------------------------- # + +# This file gathers resources to be injected in the Actions class +# +# This is the generic version + +FILE.text = File +FILE.toolTipText = Management of image files + +STEP.text = Step +STEP.toolTipText = Sequence of steps + +SCORE.text = Score +SCORE.toolTipText = Management of scores + +MIDI.text = Midi +MIDI.toolTipText = Management of Midi features + +VIEW.text = Views +VIEW.toolTipText = View Parameters + +TOOL.text = Tools +TOOL.toolTipText = Various tools + +PLUGIN.text = Plugins +PLUGIN.toolTipText = Declared plugins + +HELP.text = Help +HELP.toolTipText = Help information + +DEBUG.text = Debug +DEBUG.toolTipText = Miscellaneous debug actions diff --git a/src/main/omr/action/resources/Actions_fr.properties b/src/main/omr/action/resources/Actions_fr.properties new file mode 100644 index 0000000..6e65637 --- /dev/null +++ b/src/main/omr/action/resources/Actions_fr.properties @@ -0,0 +1,35 @@ +# ---------------------------------------------------------------------------- # +# # +# A c t i o n s _ f r . p r o p e r t i e s # +# # +# ---------------------------------------------------------------------------- # + +# This file gathers resources to be injected in the Actions class +# +# This is the FR version + +FILE.text = Fichier +FILE.toolTipText = Gestion des feuilles + +STEP.text = Etape +STEP.toolTipText = S\u00e9quence des \u00e9tapes + +SCORE.text = Partition +SCORE.toolTipText = Gestion des partitions + +MIDI.toolTipText = Gestion Midi + +VIEW.text = Vues +VIEW.toolTipText = Param\u00e8tres des vues + +TOOL.text = Outils +TOOL.toolTipText = Outils divers + +PLUGIN.text = Plugins +PLUGIN.toolTipText = Plugins d\u00e9clar\u00e9s + +HELP.text = Aide +HELP.toolTipText = Information d'aide + +DEBUG.text = Debug +DEBUG.toolTipText = Diverses actions de debugging diff --git a/src/main/omr/check/Check.java b/src/main/omr/check/Check.java new file mode 100644 index 0000000..0bbdd01 --- /dev/null +++ b/src/main/omr/check/Check.java @@ -0,0 +1,353 @@ +//----------------------------------------------------------------------------// +// // +// C h e c k // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.check; + +import omr.constant.Constant; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Class {@code Check} encapsulates the definition of a check, + * which can later be used on a whole population of objects. + * + *

The result of using a check on a given object is not recorded in this + * class, but into the checked entity itself.

+ * + *

Checks can be gathered in check suites.

+ * + * @param precise type of the objects to be checked + * + * @author Hervé Bitteur + */ +public abstract class Check +{ + //~ Static fields/initializers --------------------------------------------- + + /** Usual logger utility */ + private static final Logger logger = LoggerFactory.getLogger(Check.class); + + /** + * Indicates a negative result + */ + public static final int RED = -1; + + /** + * Indicates a non-concluding result + */ + public static final int ORANGE = 0; + + /** + * Indicates a positive result + */ + public static final int GREEN = 1; + + //~ Instance fields -------------------------------------------------------- + /** + * Specifies the FailureResult to be assigned to the Checkable object, if + * the result of the check end in the RED range. + */ + private final FailureResult redResult; + + /** Longer description, meant for tips */ + private final String description; + + /** Short name for this test */ + private final String name; + + /** + * Specifies if values are RED,ORANGE,GREEN (higher is better, covariant = + * true) or GREEN,ORANGE,RED (lower is better, covariant = false) + */ + private final boolean covariant; + + /** + * Lower bound for ORANGE range. Whatever the value of 'covariant', we must + * always have low <= high + */ + private Constant.Double low; + + /** + * Higher bound for ORANGE range. Whatever the value of 'covariant', we must + * always have low <= high + */ + private Constant.Double high; + + //~ Constructors ----------------------------------------------------------- + //-------// + // Check // + //-------// + /** + * Creates a new Check object. + * + * @param name short name for this check + * @param description longer description + * @param low lower bound of orange zone + * @param high upper bound of orange zone + * @param covariant true if higher is better, false otherwise + * @param redResult result code to be assigned when result is RED + */ + protected Check (String name, + String description, + Constant.Double low, + Constant.Double high, + boolean covariant, + FailureResult redResult) + { + this.name = name; + this.description = description; + this.low = low; + this.high = high; + this.covariant = covariant; + this.redResult = redResult; + + verifyRange(); + } + + //~ Methods ---------------------------------------------------------------- + //----------------// + // getDescription // + //----------------// + /** + * Report the related description + * + * @return the description assigned to this check + */ + public String getDescription () + { + return description; + } + + //---------// + // getHigh // + //---------// + /** + * Report the higher bound value + * + * @return the high bound + */ + public double getHigh () + { + return high.getValue(); + } + + //-----------------// + // getHighConstant // + //-----------------// + /** + * Report the higher bound constant + * + * @return the high bound constant + */ + public Constant.Double getHighConstant () + { + return high; + } + + //--------// + // getLow // + //--------// + /** + * Report the lower bound value + * + * @return the low bound + */ + public double getLow () + { + return low.getValue(); + } + + //----------------// + // getLowConstant // + //----------------// + /** + * Report the lower bound constant + * + * @return the low bound constant + */ + public Constant.Double getLowConstant () + { + return low; + } + + //---------// + // getName // + //---------// + /** + * Report the related name + * + * @return the name assigned to this check + */ + public String getName () + { + return name; + } + + //-------------// + // isCovariant // + //-------------// + /** + * Report the covariant flag + * + * @return the value of covariant flag + */ + public boolean isCovariant () + { + return covariant; + } + + //------// + // pass // + //------// + /** + * Actually run the check on the provided object, and return the result. As + * a side-effect, a check that totally fails (RED result) assigns this + * failure into the candidate object. + * + * @param obj the checkable object to be checked + * @param result output for the result, or null + * @param update true if obj is to be updated with the result + * + * @return the result composed of the numerical value, plus a flag ({@link + * #RED}, {@link #ORANGE}, {@link #GREEN}) that characterizes the result of + * passing the check on this object + */ + public CheckResult pass (C obj, + CheckResult result, + boolean update) + { + if (result == null) { + result = new CheckResult(); + } + + result.value = getValue(obj); + + if (covariant) { + if (result.value < low.getValue()) { + if (update) { + obj.setResult(redResult); + } + + result.flag = RED; + } else if (result.value >= high.getValue()) { + result.flag = GREEN; + } else { + result.flag = ORANGE; + } + } else { + if (result.value <= low.getValue()) { + result.flag = GREEN; + } else if (result.value > high.getValue()) { + if (update) { + obj.setResult(redResult); + } + + result.flag = RED; + } else { + result.flag = ORANGE; + } + } + + return result; + } + + //------------// + // setLowHigh // + //------------// + /** + * Allows to set the pair of low and high value. They are set in one shot to + * allow the sanity check of 'low' less than or equal to 'high' + * + * @param low the new low value + * @param high the new high value + */ + public void setLowHigh (Constant.Double low, + Constant.Double high) + { + this.low = low; + this.high = high; + verifyRange(); + } + + //----------// + // toString // + //----------// + /** + * report a readable description of this check + */ + @Override + public String toString () + { + StringBuilder sb = new StringBuilder(128); + sb.append("{Check ") + .append(name); + sb.append(" Covariant:") + .append(covariant); + sb.append(" Low:") + .append(low); + sb.append(" High:") + .append(high); + sb.append("}"); + + return sb.toString(); + } + + //----------// + // getValue // + //----------// + /** + * Method to be provided by any concrete subclass, in order to retrieve the + * proper data value from the given object passed as a parameter. + * + * @param obj the object to be checked + * + * @return the data value relevant for the check + */ + protected abstract double getValue (C obj); + + //-------------// + // verifyRange // + //-------------// + private void verifyRange () + { + if (low.getValue() > high.getValue()) { + logger.error( + "Illegal low {} high {} range for {}", + low.getValue(), high.getValue(), this); + } + } + + //~ Inner Classes ---------------------------------------------------------- + //-------// + // Grade // + //-------// + /** + * A subclass of Constant.Double, meant to store a check result grade. + */ + public static class Grade + extends Constant.Double + { + //~ Constructors ------------------------------------------------------- + + /** + * Specific constructor, where 'unit' and 'name' are assigned later + * + * @param defaultValue the (double) default value + * @param description the semantic of the constant + */ + public Grade (double defaultValue, + java.lang.String description) + { + super("Grade", defaultValue, description); + } + } +} diff --git a/src/main/omr/check/CheckBoard.java b/src/main/omr/check/CheckBoard.java new file mode 100644 index 0000000..db31100 --- /dev/null +++ b/src/main/omr/check/CheckBoard.java @@ -0,0 +1,163 @@ +//----------------------------------------------------------------------------// +// // +// C h e c k B o a r d // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.check; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import omr.selection.MouseMovement; +import omr.selection.SelectionService; +import omr.selection.UserEvent; + +import omr.ui.Board; + +import com.jgoodies.forms.builder.PanelBuilder; +import com.jgoodies.forms.layout.CellConstraints; +import com.jgoodies.forms.layout.FormLayout; + +/** + * Class {@code CheckBoard} defines a board dedicated to the display of + * check result information. + * + * @param The {@link Checkable} entity type to be checked + * + * @author Hervé Bitteur + */ +public class CheckBoard + extends Board +{ + //~ Static fields/initializers --------------------------------------------- + + /** Usual logger utility */ + private static final Logger logger = LoggerFactory.getLogger(CheckBoard.class); + + //~ Instance fields -------------------------------------------------------- + // + /** For display of check suite results */ + private final CheckPanel checkPanel; + + //~ Constructors ----------------------------------------------------------- + // + //------------// + // CheckBoard // + //------------// + /** + * Create a Check Board. + * + * @param name the name of the check + * @param suite the check suite to be used + * @param selectionService which selection service to use + * @param eventList which event classes to expect + */ + public CheckBoard (String name, + CheckSuite suite, + SelectionService selectionService, + Class[] eventList) + { + super(name, + Board.CHECK.position, + selectionService, + eventList, + false, // Dump + false); // Selected + checkPanel = new CheckPanel<>(suite); + + if (suite != null) { + defineLayout(suite.getName()); + } + + // define default content + tellObject(null); + } + + //~ Methods ---------------------------------------------------------------- + // + //---------// + // onEvent // + //---------// + /** + * Call-back triggered when C Selection has been modified. + * + * @param event the Event to perform check upon its data + */ + @Override + @SuppressWarnings("unchecked") + public void onEvent (UserEvent event) + { + try { + // Ignore RELEASING + if (event.movement == MouseMovement.RELEASING) { + return; + } + + tellObject((C) event.getData()); // Compiler warning + } catch (Exception ex) { + logger.warn(getClass().getName() + " onEvent error", ex); + } + } + + //------------// + // applySuite // + //------------// + /** + * Assign a (new) suite to the check board and apply it to the + * provided object. + * + * @param suite the (new) check suite to be used + * @param object the object to apply the checks suite on + */ + public synchronized void applySuite (CheckSuite suite, + C object) + { + final boolean toBuild = checkPanel.getComponent() == null; + checkPanel.setSuite(suite); + + if (toBuild) { + defineLayout(suite.getName()); + } + + tellObject(object); + } + + //------------// + // tellObject // + //------------// + /** + * Render the result of checking the given object. + * + * @param object the object whose check result is to be displayed + */ + protected final void tellObject (C object) + { + if (object == null) { + setVisible(false); + } else { + setVisible(true); + checkPanel.passForm(object); + } + } + + //--------------// + // defineLayout // + //--------------// + private void defineLayout (String name) + { + FormLayout layout = new FormLayout("pref", "pref"); + PanelBuilder builder = new PanelBuilder(layout, getBody()); + builder.setDefaultDialogBorder(); + + CellConstraints cst = new CellConstraints(); + + int r = 1; // -------------------------------- + builder.add(checkPanel.getComponent(), cst.xy(1, r)); + } +} diff --git a/src/main/omr/check/CheckPanel.java b/src/main/omr/check/CheckPanel.java new file mode 100644 index 0000000..968e680 --- /dev/null +++ b/src/main/omr/check/CheckPanel.java @@ -0,0 +1,550 @@ +//----------------------------------------------------------------------------// +// // +// C h e c k P a n e l // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.check; + +import omr.constant.Constant; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import omr.ui.util.Panel; + +import com.jgoodies.forms.builder.PanelBuilder; +import com.jgoodies.forms.layout.CellConstraints; +import com.jgoodies.forms.layout.FormLayout; + +import java.awt.Color; +import java.awt.event.ActionEvent; +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; +import java.util.Scanner; + +import javax.swing.AbstractAction; +import javax.swing.JComponent; +import javax.swing.JLabel; +import javax.swing.JTextField; +import javax.swing.KeyStroke; + +/** + * Class {@code CheckPanel} handles a panel to display the results of a + * check suite. + * + * @param the subtype of Checkable-compatible objects used in the + * homogeneous collection of checks of the suite + * + * @author Hervé Bitteur + */ +public class CheckPanel +{ + //~ Static fields/initializers --------------------------------------------- + + /** Usual logger utility */ + private static final Logger logger = LoggerFactory.getLogger(CheckPanel.class); + + // Colors + private static final Color GREEN_COLOR = new Color(100, 150, 0); + + private static final Color ORANGE_COLOR = new Color(255, 150, 0); + + private static final Color RED_COLOR = new Color(255, 0, 0); + + // Sizes + private static final String LINE_GAP = "1dlu"; + + private static final String COLUMN_GAP = "1dlu"; + + private static final int FIELD_WIDTH = 4; + + //~ Instance fields -------------------------------------------------------- + // + /** The related check suite (the model) */ + private CheckSuite suite; + + /** The swing component that includes all the fields */ + private Panel component; + + /** The field for global result */ + private JTextField globalField; + + /** Matrix of all value fields */ + private JTextField[][] values; + + /** Matrix of all bound fields */ + private JTextField[][] bounds; + + /** Last object checked */ + private C object; + + //~ Constructors ----------------------------------------------------------- + // + //------------// + // CheckPanel // + //------------// + /** + * Create a check panel for a given suite. + * + * @param suite the suite whose results are to be displayed + */ + public CheckPanel (CheckSuite suite) + { + // Global field (for global result) + globalField = new JTextField(FIELD_WIDTH); + globalField.setEditable(false); + globalField.setHorizontalAlignment(JTextField.CENTER); + + setSuite(suite); + } + + //~ Methods ---------------------------------------------------------------- + // + //--------------// + // getComponent // + //--------------// + /** + * Report the UI component. + * + * @return the concrete component + */ + public JComponent getComponent () + { + return component; + } + + //----------// + // passForm // + //----------// + /** + * Pass the whole suite on the provided checkable object, and + * display the results. + * + * @param object the object to be checked + */ + public void passForm (C object) + { + // Remember the 'current' object' + this.object = object; + + resetValues(); + + if (object == null) { + return; + } + + CheckResult result = new CheckResult(); + double grade = 0d; + boolean failed = false; + + // Fill one row per check + for (int index = 0; index < suite.getChecks().size(); index++) { + Check check = suite.getChecks().get(index); + + try { + // Run this check + check.pass(object, result, false); + grade += (result.flag * suite.getWeights().get(index)); + + // Update proper field to display check result + JTextField field; + + switch (result.flag) { + case Check.RED: + failed = true; + + if (check.isCovariant()) { + field = values[index][0]; + field.setToolTipText("Value is too low"); + } else { + field = values[index][2]; + field.setToolTipText("Value is too high"); + } + + field.setForeground(RED_COLOR); + + break; + + case Check.ORANGE: + field = values[index][1]; + field.setToolTipText("Value is acceptable"); + field.setForeground(ORANGE_COLOR); + + break; + + default: + case Check.GREEN: + + if (check.isCovariant()) { + field = values[index][2]; + } else { + field = values[index][0]; + } + + field.setToolTipText("Value is OK"); + field.setForeground(GREEN_COLOR); + + break; + } + + field.setText(textOf(result.value)); + } catch (Throwable ex) { + logger.warn("Failure in check " + check.getName(), ex); + failed = true; + } + } + + // Global suite result + if (failed) { + globalField.setForeground(RED_COLOR); + globalField.setToolTipText("Check has failed!"); + globalField.setText("Failed"); + } else { + grade /= suite.getTotalWeight(); + + if (grade >= suite.getThreshold()) { + globalField.setForeground(GREEN_COLOR); + globalField.setToolTipText("Check has succeeded!"); + } else { + globalField.setForeground(RED_COLOR); + globalField.setToolTipText("Check has failed!"); + } + + globalField.setText(textOf(grade)); + } + } + + //----------// + // setSuite // + //----------// + /** + * Assign a (new) suite to the check pane. + * + * @param suite the (new) check suite to be used + */ + public final void setSuite (CheckSuite suite) + { + this.suite = suite; + + if (suite != null) { + createValueFields(); // Values + createBoundFields(); // Bounds + buildComponent(); // Create/update component + } + + // Refresh the display + if (component != null) { + component.validate(); + component.repaint(); + } + } + + //----------------// + // buildComponent // + //----------------// + private void buildComponent () + { + // Either allocate a new Panel or empty the existing one + if (component == null) { + component = new Panel(); + component.setNoInsets(); + + // Needed to process user input when RETURN/ENTER is pressed + component.getInputMap( + JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT). + put(KeyStroke.getKeyStroke("ENTER"), "ParamAction"); + component.getActionMap().put("ParamAction", new ParamAction()); + } else { + component.removeAll(); + } + + final int checkNb = suite.getChecks().size(); + PanelBuilder b = new PanelBuilder(createLayout(checkNb), component); + b.setDefaultDialogBorder(); + + CellConstraints c = new CellConstraints(); + + // Rows + int ic = -1; // Check index + int r = -1; // Row index + + for (Check check : suite.getChecks()) { + ic++; + r += 2; + + // Covariance label + JLabel covariantLabel; + + if (check.isCovariant()) { + covariantLabel = new JLabel(">"); + covariantLabel.setToolTipText("Higher is better"); + } else { + covariantLabel = new JLabel("<"); + covariantLabel.setToolTipText("Lower is better"); + } + + b.add(covariantLabel, c.xy(1, r)); + + // Name label with proper tooltip + JLabel nameLabel = new JLabel(check.getName()); + + if (check.getDescription() != null) { + StringBuilder sb = new StringBuilder(); + sb.append(""); + sb.append(check.getDescription()); + + Constant constant = check.getLowConstant(); + + // Tell data unit if relevant + sb.append("
"); + + if (constant.getQuantityUnit() != null) { + sb.append("Unit=").append(constant.getQuantityUnit()); + } else { + // Otherwise, simply tell the data type + sb.append("Type=").append(constant.getShortTypeName()); + } + + sb.append(""); + + nameLabel.setToolTipText(sb.toString()); + } + + b.add(nameLabel, c.xy(3, r)); + + // Value & bound fields + b.add(values[ic][0], c.xy(5, r)); + b.add(bounds[ic][0], c.xy(7, r)); + b.add(values[ic][1], c.xy(9, r)); + b.add(bounds[ic][1], c.xy(11, r)); + b.add(values[ic][2], c.xy(13, r)); + } + + // Last row for global result + r += 2; + + JLabel globalLabel = new JLabel("Result"); + globalLabel.setToolTipText("Global check result"); + b.add(globalLabel, c.xy(5, r)); + b.add(globalField, c.xy(9, r)); + } + + //-------------------// + // createBoundFields // + //-------------------// + private void createBoundFields () + { + // Allocate bound fields (2 per check) + final int checkNb = suite.getChecks().size(); + bounds = new JTextField[checkNb][]; + + for (int ic = 0; ic < checkNb; ic++) { + Check check = suite.getChecks().get(ic); + bounds[ic] = new JTextField[2]; + + for (int i = 0; i <= 1; i++) { + JTextField field = new JTextField(FIELD_WIDTH); + field.setHorizontalAlignment(JTextField.CENTER); + bounds[ic][i] = field; + + Constant.Double constant = (i == 0) ? check.getLowConstant() + : check.getHighConstant(); + + field.setText(textOf(constant.getValue())); + field.setToolTipText( + "" + constant.getName() + "
" + + constant.getDescription() + ""); + } + } + } + + //--------------// + // createLayout // + //--------------// + private FormLayout createLayout (int checkNb) + { + // Build proper column specification + StringBuilder sbc = new StringBuilder(); + sbc.append("center:pref").append(", ").append(COLUMN_GAP).append(", "); // Covariance + sbc.append(" right:40dlu").append(", ").append(COLUMN_GAP).append(", "); // Name + sbc.append(" right:pref").append(", ").append(COLUMN_GAP).append(", "); + sbc.append(" right:pref").append(", ").append(COLUMN_GAP).append(", "); // Low limit + sbc.append(" right:pref").append(", ").append(COLUMN_GAP).append(", "); + sbc.append(" right:pref").append(", ").append(COLUMN_GAP).append(", "); // High Limit + sbc.append(" right:pref"); + + // Build proper row specification + StringBuilder sbr = new StringBuilder(); + + for (int n = 0; n <= checkNb; n++) { + if (n != 0) { + sbr.append(", ").append(LINE_GAP).append(", "); + } + + sbr.append("pref"); + } + + logger.debug("sb cols={}", sbc); + logger.debug("sb rows={}", sbr); + + // Create proper form layout + return new FormLayout( + sbc.toString(), //cols + sbr.toString()); //rows + } + + //-------------------// + // createValueFields // + //-------------------// + private void createValueFields () + { + // Allocate value fields (3 per check) + final int checkNb = suite.getChecks().size(); + values = new JTextField[checkNb][3]; + + for (int n = 0; n < checkNb; n++) { + for (int i = 0; i <= 2; i++) { + JTextField field = new JTextField(FIELD_WIDTH); + field.setEditable(false); + field.setHorizontalAlignment(JTextField.CENTER); + values[n][i] = field; + } + } + } + + //-------------// + // resetValues // + //-------------// + private void resetValues () + { + for (JTextField[] seq : values) { + for (JTextField field : seq) { + field.setText(""); + } + } + } + + //--------// + // textOf // + //--------// + private String textOf (double val) + { + return String.format(Locale.getDefault(), "%5.2f", val); + } + + //---------// + // valueOf // + //---------// + private double valueOf (String text) + { + Scanner scanner = new Scanner(text); + scanner.useLocale(Locale.getDefault()); + + while (scanner.hasNext()) { + if (scanner.hasNextDouble()) { + return scanner.nextDouble(); + } else { + scanner.next(); + } + } + + // Kludge! + return Double.NaN; + } + + //~ Inner Classes ---------------------------------------------------------- + // + //-------------// + // ParamAction // + //-------------// + private class ParamAction + extends AbstractAction + { + //~ Methods ------------------------------------------------------------ + + /** + * Method run whenever user presses Return/Enter in one of + * the parameter fields + */ + @Override + public void actionPerformed (ActionEvent e) + { + // Any & several bounds may have been modified by the user + // Since the same constant can be used in several fields, we have to + // take a snapshot of all constants values, before modifying any one + Map values = new HashMap<>(); + + for (Check check : suite.getChecks()) { + values.put( + check.getLowConstant(), + check.getLowConstant().getValue()); + values.put( + check.getHighConstant(), + check.getHighConstant().getValue()); + } + + boolean modified = false; + int ic = -1; + + for (Check check : suite.getChecks()) { + ic++; + + // Check the bounds wrt the corresponding fields + for (int i = 0; i < 2; i++) { + final Constant.Double constant = (i == 0) + ? check.getLowConstant() + : check.getHighConstant(); + + // Simplistic test to detect modification + final JTextField field = bounds[ic][i]; + final String oldString = textOf(values.get(constant)).trim(); + final String newString = field.getText().trim(); + + if (!oldString.equals(newString)) { + StringBuilder sb = new StringBuilder(); + sb.append("Check '").append(check.getName()). + append("':"); + + if (i == 0) { + sb.append(" Low"); + } else { + sb.append(" High"); + } + + sb.append(" bound '").append(constant.getName()).append( + "'"); + + final String context = sb.toString(); + + // Actually convert the value and update the constant + try { + constant.setValue(valueOf(newString)); + modified = true; + sb.append(" modified from ").append(oldString). + append(" to ").append(newString); + logger.info(sb.toString()); + } catch (Exception ex) { + logger.warn( + "Error in {}, {}", + context, ex.getLocalizedMessage()); + } + } + } + } + + // If at least one modification has been made, update the whole + // table with both suite parameters and object results + if (modified) { + setSuite(suite); + passForm(object); + } + } + } +} diff --git a/src/main/omr/check/CheckResult.java b/src/main/omr/check/CheckResult.java new file mode 100644 index 0000000..cd25c3f --- /dev/null +++ b/src/main/omr/check/CheckResult.java @@ -0,0 +1,30 @@ +//----------------------------------------------------------------------------// +// // +// C h e c k R e s u l t // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.check; + + +/** + * Class {@code CheckResult} encapsulates the result of a check, + * composed of a value (double) and a flag which can be RED, YELLOW or GREEN. + * + * @author Hervé Bitteur + */ +public class CheckResult +{ + //~ Instance fields -------------------------------------------------------- + + /** Numerical result value */ + public double value; + + /** Flag the result (RED, YELLOW, GREEN) */ + public int flag; +} diff --git a/src/main/omr/check/CheckSuite.java b/src/main/omr/check/CheckSuite.java new file mode 100644 index 0000000..5ce7c0d --- /dev/null +++ b/src/main/omr/check/CheckSuite.java @@ -0,0 +1,351 @@ +//----------------------------------------------------------------------------// +// // +// C h e c k S u i t e // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.check; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +/** + * Class {@code CheckSuite} represents a suite of homogeneous checks, + * that is checks working on the same type. + * + * Every check in the suite is assigned a weight, to represent its relative + * importance in the suite. + * + * @param the subtype of Checkable-compatible objects used in the + * homogeneous collection of checks in this suite + * + * @author Hervé Bitteur + */ +public class CheckSuite +{ + //~ Static fields/initializers --------------------------------------------- + + /** Usual logger utility */ + private static final Logger logger = LoggerFactory.getLogger(CheckSuite.class); + + //~ Instance fields -------------------------------------------------------- + /** Name of this suite */ + protected String name; + + /** Minimum threshold for final grade */ + protected double threshold; + + /** List of checks in the suite */ + private final List> checks = new ArrayList<>(); + + /** List of related weights in the suite */ + private final List weights = new ArrayList<>(); + + /** Total checks weight */ + private double totalWeight = 0.0d; + + //~ Constructors ----------------------------------------------------------- + //------------// + // CheckSuite // + //------------// + /** + * Create a suite of checks + * + * @param name the name for the suite (for debug) + * @param threshold the threshold to test results + */ + public CheckSuite (String name, + double threshold) + { + this.name = name; + this.threshold = threshold; + } + + //~ Methods ---------------------------------------------------------------- + //-----// + // add // + //-----// + /** + * Add a check to the suite, with its assigned weight + * + * @param weight the weight of this check in the suite + * @param check the check to add to the suite + */ + public void add (double weight, + Check check) + { + checks.add(check); + weights.add(weight); + totalWeight += weight; + } + + //--------// + // addAll // + //--------// + /** + * Add all checks of another suite + * + * @param suite the suite of checks to be appended + * @return the suite with checks appended, for easy chaining + */ + public CheckSuite addAll (CheckSuite suite) + { + int index = 0; + + for (Check check : suite.checks) { + double weight = suite.weights.get(index++); + add(weight, check); + } + + // Allow chaining + return this; + } + + //------// + // dump // + //------// + /** + * Dump a readable description of all checks of this suite. + */ + public void dump () + { + StringBuilder sb = new StringBuilder(String.format("%n")); + + if (name != null) { + sb.append(name); + } + + sb.append(String.format(" Check Suite: threshold=%f%n", threshold)); + + dumpSpecific(sb); + + sb.append(String.format( + "Weight Name Covariant Low High%n")); + sb.append(String.format( + "------ ---- ------ --- ----%n")); + + int index = 0; + + for (Check check : checks) { + sb.append(String.format( + "%4.1f %-19s %5b % 6.2f % 6.2f %n", + weights.get(index++), + check.getName(), + check.isCovariant(), + check.getLow(), + check.getHigh())); + } + + logger.info(sb.toString()); + } + + //-----------// + // getChecks // + //-----------// + /** + * Report the collection of checks that compose this suite. + * + * @return the collection of checks + */ + public List> getChecks () + { + return checks; + } + + //---------// + // getName // + //---------// + /** + * Report the name of this suite. + * + * @return suite name + */ + public String getName () + { + return name; + } + + //--------------// + // getThreshold // + //--------------// + /** + * Report the assigned threshold. + * + * @return the assigned minimum result + */ + public double getThreshold () + { + return threshold; + } + + //----------------// + // getTotalWeight // + //----------------// + /** + * Report the sum of all individual checks. + * + * @return the total weight of the checks in the suite + */ + public double getTotalWeight () + { + return totalWeight; + } + + //------------// + // getWeights // + //------------// + /** + * Report the weights of the checks. + * (collection parallel to the suite checks) + * + * @return the collection of checks weights + */ + public List getWeights () + { + return weights; + } + + //------// + // pass // + //------// + /** + * Pass sequentially the checks in the suite, stopping at the first + * test with red result. + * + * @param object the object to be checked + * @return the computed grade. + */ + public double pass (C object) + { + boolean debug = logger.isDebugEnabled() || object.isVip(); + double grade = 0.0d; + CheckResult result = new CheckResult(); + StringBuilder sb = null; + + if (debug) { + sb = new StringBuilder(512); + sb.append(name).append(" ").append(object).append(" "); + } + + int index = 0; + + for (Check check : checks) { + check.pass(object, result, true); + + if (debug) { + sb.append( + String.format("%15s:%5.2f", check.getName(), + result.value)); + } + + if (result.flag == Check.RED) { + // The check totally failed, we give up immediately! + if (debug) { + logger.info(sb.toString()); + } + + return result.flag; + } else { + // Aggregate results + double weight = weights.get(index); + grade += (result.flag * weight); + } + + index++; + } + + // Final grade + grade /= totalWeight; + + if (debug) { + sb.append(String.format(" => %5.2f ", grade)); + logger.info(sb.toString()); + } + + return grade; + } + + //----------------// + // passCollection // + //----------------// + /** + * Pass the whole collection of suites in a row and return + * the global result. + * + * @param The specific type of checked object + * @param object the object to be checked + * @param suites the collection of check suites to pass + * + * @return the global result + */ + public static double passCollection (T object, + Collection> suites) + { + double totalWeight = 0.0d; + double grade = 0.0d; + + for (CheckSuite suite : suites) { + double res = suite.pass(object); + + // If one totally failed, give up immediately + if (res == Check.RED) { + return res; + } else { + // Aggregate results + double weight = suite.getTotalWeight(); + totalWeight += weight; + grade += (res * weight); + } + } + + // Final grade + return grade / totalWeight; + } + + //---------// + // setName // + //---------// + /** + * Assings a new name to the check suite. + * + * @param name the new name + */ + public void setName (String name) + { + this.name = name; + } + + //--------------// + // setThreshold // + //--------------// + /** + * Allows to assign a new threshold for the suite. + * + * @param threshold the new minimum result + */ + public void setThreshold (double threshold) + { + this.threshold = threshold; + } + + //--------------// + // dumpSpecific // + //--------------// + /** + * Just an empty placeholder, meant to be overridden. + * + * @param sb StringBuilder to populate + */ + protected void dumpSpecific (StringBuilder sb) + { + } +} diff --git a/src/main/omr/check/Checkable.java b/src/main/omr/check/Checkable.java new file mode 100644 index 0000000..7bcf8de --- /dev/null +++ b/src/main/omr/check/Checkable.java @@ -0,0 +1,33 @@ +//----------------------------------------------------------------------------// +// // +// C h e c k a b l e // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.check; + +import omr.util.Vip; + +/** + * Interface {@code Checkable} describes a class that may be checked and + * then assigned a result for that check. + * + * @author Hervé Bitteur + */ +public interface Checkable + extends Vip +{ + //~ Methods ---------------------------------------------------------------- + + /** + * Store the check result directly into the checkable entity. + * + * @param result the result to be stored + */ + void setResult (Result result); +} diff --git a/src/main/omr/check/FailureResult.java b/src/main/omr/check/FailureResult.java new file mode 100644 index 0000000..5c33d6c --- /dev/null +++ b/src/main/omr/check/FailureResult.java @@ -0,0 +1,54 @@ +//----------------------------------------------------------------------------// +// // +// F a i l u r e R e s u l t // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.check; + + +/** + * Class {@code FailureResult} is the root of all results that store a + * failure. + * + * @author Hervé Bitteur + */ +public class FailureResult + extends Result +{ + //~ Constructors ----------------------------------------------------------- + + //---------------// + // FailureResult // + //---------------// + /** + * Create a new FailureResult object. + * + * @param comment A comment that describe the failure reason + */ + public FailureResult (String comment) + { + super(comment); + } + + //~ Methods ---------------------------------------------------------------- + + //----------// + // toString // + //----------// + /** + * Report a readable string about this failure instance + * + * @return a descriptive string + */ + @Override + public String toString () + { + return "Failure:" + super.toString(); + } +} diff --git a/src/main/omr/check/Result.java b/src/main/omr/check/Result.java new file mode 100644 index 0000000..f9a202f --- /dev/null +++ b/src/main/omr/check/Result.java @@ -0,0 +1,60 @@ +//----------------------------------------------------------------------------// +// // +// R e s u l t // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.check; + + +/** + * Class {@code Result} is the root of all result information stored while + * processing processing checks. + * + * @author Hervé Bitteur + */ +public abstract class Result +{ + //~ Instance fields -------------------------------------------------------- + + /** + * A readable comment about the result. + */ + public final String comment; + + //~ Constructors ----------------------------------------------------------- + + //--------// + // Result // + //--------// + /** + * Creates a new Result object. + * + * @param comment A description of this result + */ + public Result (String comment) + { + this.comment = comment; + } + + //~ Methods ---------------------------------------------------------------- + + //----------// + // toString // + //----------// + /** + * Report a description of this result + * + * @return A descriptive string + */ + @Override + public String toString () + { + return comment; + } +} diff --git a/src/main/omr/check/SuccessResult.java b/src/main/omr/check/SuccessResult.java new file mode 100644 index 0000000..7716ccd --- /dev/null +++ b/src/main/omr/check/SuccessResult.java @@ -0,0 +1,54 @@ +//----------------------------------------------------------------------------// +// // +// S u c c e s s R e s u l t // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.check; + + +/** + * Class {@code SuccessResult} is the root of all results that store a + * success. + * + * @author Hervé Bitteur + */ +public class SuccessResult + extends Result +{ + //~ Constructors ----------------------------------------------------------- + + //---------------// + // SuccessResult // + //---------------// + /** + * Creates a new SuccessResult object. + * + * @param comment A description of the success + */ + public SuccessResult (String comment) + { + super(comment); + } + + //~ Methods ---------------------------------------------------------------- + + //----------// + // toString // + //----------// + /** + * Report a description of this success + * + * @return A descriptive string + */ + @Override + public String toString () + { + return "Success:" + super.toString(); + } +} diff --git a/src/main/omr/check/package.html b/src/main/omr/check/package.html new file mode 100644 index 0000000..77612d1 --- /dev/null +++ b/src/main/omr/check/package.html @@ -0,0 +1,21 @@ + + + + + + Package omr.check + + + +

+ This package defines checks, organized in check suites, to run + series of checks to filter out some candidates when looking for + precise entities. + This filtering technique is quite rough, and + should perhaps be replaced by proper use of specific neural + networks. +

+ + + diff --git a/src/main/omr/constant/Constant.java b/src/main/omr/constant/Constant.java new file mode 100644 index 0000000..3e787e3 --- /dev/null +++ b/src/main/omr/constant/Constant.java @@ -0,0 +1,923 @@ +//----------------------------------------------------------------------------// +// // +// C o n s t a n t // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.constant; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import omr.util.DoubleValue; + +import net.jcip.annotations.ThreadSafe; + +import java.util.concurrent.atomic.AtomicReference; + +/** + * This abstract class handles the mapping between one application + * variable and a property name and value. + * It is meant essentially to handle any kind of symbolic constant, whose value + * may have to be tuned and saved for future runs of the application. + * + *

Please refer to {@link ConstantManager} for a detailed explanation on how + * the current value of any given Constant is determined at run-time. + * + *

The class {@code Constant} is not meant to be used directly (it is + * abstract), but rather through any of its subclasses: + * + *

  • {@link Constant.Angle}
  • {@link Constant.Boolean}
  • + *
  • {@link Constant.Color}
  • {@link Constant.Double}
  • + * {@link Constant.Integer}
  • {@link Constant.Ratio}
  • + *
  • {@link Constant.String}
  • + *
  • and others...

+ * + * @author Hervé Bitteur + */ +@ThreadSafe +public abstract class Constant +{ + //~ Static fields/initializers --------------------------------------------- + + /** Usual logger utility */ + private static final Logger logger = LoggerFactory.getLogger(Constant.class); + + //~ Instance fields -------------------------------------------------------- + // + // Data assigned at construction time + //----------------------------------- + /** Unit (if relevant) used by the quantity measured. */ + private final java.lang.String quantityUnit; + + /** Source-provided value to be used if needed. */ + private final java.lang.String sourceString; + + /** Semantic. */ + private final java.lang.String description; + + // Data assigned at ConstantSet initMap time + //------------------------------------------ + /** Name of the Constant. */ + private volatile java.lang.String name; + + /** Fully qualified Constant name. (unit.name) */ + private volatile java.lang.String qualifiedName; + + // Data modified at any time + //-------------------------- + /** Initial Value (used for reset). Assigned once */ + private java.lang.String initialString; + + /** Current data. */ + private AtomicReference tuple = new AtomicReference<>(); + + //~ Constructors ----------------------------------------------------------- + // + //----------// + // Constant // + //----------// + /** + * Creates a constant instance, while providing a default value, + * in case the external property is not yet defined. + * + * @param quantityUnit Unit used as base for measure, if relevant + * @param sourceString Source value, expressed by a string literal which + * cannot be null + * @param description A quick description of the purpose of this constant + */ + protected Constant (java.lang.String quantityUnit, + java.lang.String sourceString, + java.lang.String description) + { + if (sourceString == null) { + logger.warn( + "*** Constant with no sourceString. Description: {}", + description); + throw new IllegalArgumentException( + "Any constant must have a source-provided String"); + } + + this.quantityUnit = quantityUnit; + this.sourceString = sourceString; + this.description = description; + + // System.out.println( + // Thread.currentThread().getName() + ": " + "-- Creating Constant: " + + // description); + } + + //~ Methods ---------------------------------------------------------------- + // + //----------// + // setValue // + //----------// + /** + * Modify the current value of the constant. + * This abstract method is actually defined in each subclass, to enforce + * validation of the provided string with respect to the target constant type. + * + * @param string the new value, as a string to be checked + */ + public abstract void setValue (java.lang.String string); + + //------------------// + // getCurrentString // + //------------------// + /** + * Get the current value, as a String type. + * + * @return the String view of the value + */ + public java.lang.String getCurrentString () + { + return getTuple().currentString; + } + + //----------------// + // getDescription // + //----------------// + /** + * Get the description sentence recorded with the constant + * + * @return the description sentence as a string + */ + public java.lang.String getDescription () + { + return description; + } + + //---------// + // getName // + //---------// + /** + * Report the name of the constant + * + * @return the constant name + */ + public java.lang.String getName () + { + return name; + } + + //------------------// + // getQualifiedName // + //------------------// + /** + * Report the qualified name of the constant + * + * @return the constant qualified name + */ + public java.lang.String getQualifiedName () + { + return qualifiedName; + } + + //-----------------// + // getQuantityUnit // + //-----------------// + /** + * Report the unit, if any, used as base of quantity measure + * + * @return the quantity unit, if any + */ + public java.lang.String getQuantityUnit () + { + return quantityUnit; + } + + //------------------// + // getShortTypeName // + //------------------// + /** + * Report the very last part of the type name of the constant + * + * @return the type name (last part) + */ + public java.lang.String getShortTypeName () + { + final java.lang.String typeName = getClass().getName(); + final int separator = Math.max( + typeName.lastIndexOf('$'), + typeName.lastIndexOf('.')); + + if (separator != -1) { + return typeName.substring(separator + 1); + } else { + return typeName; + } + } + + //-----------------// + // getSourceString // + //-----------------// + /** + * Report the constant source string + * + * @return the source string + */ + public java.lang.String getSourceString () + { + return sourceString; + } + + //------------// + // isModified // + //------------// + /** + * Checks whether the current value is different from the original one. + * NOTA_BENE: The test is made on string literal, which may result in false + * modification signals, simply because the string for example contains an + * additional space + * + * @return The modification status + */ + public boolean isModified () + { + return !getCurrentString().equals(initialString); + } + + //---------------// + // isSourceValue // + //---------------// + /** + * Report whether the current constant value is the source one. + * (not altered by either properties read from disk, of value changed later) + * + * @return true if still the source value, false otherwise + */ + public boolean isSourceValue () + { + return getCurrentString().equals(sourceString); + } + + //--------// + // remove // + //--------// + /** + * Remove a given constant from memory + */ + public void remove () + { + ConstantManager.getInstance().removeConstant(this); + } + + //-------// + // reset // + //-------// + /** + * Forget any modification made, and reset to the source value. + */ + public void reset () + { + setTuple(sourceString, decode(sourceString)); + } + + //---------// + // setUnit // + //---------// + /** + * Allows to record the unit and name of the constant. + * + * @param unit the unit (class name) this constant belongs to + * @param name the constant name + */ + public void setUnitAndName (java.lang.String unit, + java.lang.String name) + { + // System.out.println( + // Thread.currentThread().getName() + ": " + "Assigning unit:" + unit + + // " name:" + name); + this.name = name; + + final java.lang.String qName = (unit != null) ? (unit + "." + name) : name; + + // We can now try to register that constant + try { + java.lang.String prop = ConstantManager.getInstance().addConstant( + qName, this); + + // Now we can assign a first current value + if (prop != null) { + // Use property value + setTuple(prop, decode(prop)); + } else { + // Use source value + ///logger.info("setUnitAndName. unit:" + unit + " name:" + name); + setTuple(sourceString, decode(sourceString)); + } + + // Very last thing + qualifiedName = qName; + + // System.out.println( + // Thread.currentThread().getName() + ": " + "Done unit:" + unit + + // " name:" + name); + } catch (Exception ex) { + logger.warn("Error registering constant {}", qName); + ex.printStackTrace(); + } + } + + //------------------// + // toDetailedString // + //------------------// + /** + * Report detailed data about this constant + * + * @return data meant for end user + */ + public java.lang.String toDetailedString () + { + StringBuilder sb = new StringBuilder(getQualifiedName()); + sb.append(" (").append(getCurrentString()).append(")"); + sb.append(" \"").append(getDescription()).append("\""); + + return sb.toString(); + } + + //----------// + // toString // + //----------// + /** + * Used by UnitTreeTable to display the name of the constant, + * so only the unqualified name is returned. + * + * @return the (unqualified) constant name + */ + @Override + public java.lang.String toString () + { + return (name != null) ? name : "*no name*"; + } + + //--------// + // decode // + //--------// + /** + * Convert a given string to the proper object value, as + * implemented by each subclass. + * + * @param str the encoded string + * @return the decoded object + */ + protected abstract Object decode (java.lang.String str); + + //----------------// + // getCachedValue // + //----------------// + /** + * Report the current value of the constant + * + * @return the (cached) current value + */ + protected Object getCachedValue () + { + return getTuple().cachedValue; + } + + //----------// + // setTuple // + //----------// + /** + * Modify the current parameter data in an atomic way, + * and remember the very first value (the initial string). + * + * @param str The new value (as a string) + * @param val The new value (as an object) + */ + protected void setTuple (java.lang.String str, + Object val) + { + while (true) { + Tuple old = tuple.get(); + Tuple temp = new Tuple(str, val); + + if (old == null) { + if (tuple.compareAndSet(null, temp)) { + initialString = str; + + return; + } + } else { + tuple.set(temp); + + return; + } + } + } + + //----------------// + // getValueOrigin // + //----------------// + /** + * Convenient method, reporting the origin of the current value for + * this constant, either SRC or USR. + * + * @return a mnemonic for the value origin + */ + java.lang.String getValueOrigin () + { + ConstantManager mgr = ConstantManager.getInstance(); + java.lang.String cur = getCurrentString(); + java.lang.String usr = mgr.getConstantUserValue(qualifiedName); + java.lang.String src = sourceString; + + if (cur.equals(src)) { + return "SRC"; + } + + if (cur.equals(usr)) { + return "USR"; + } + + return "???"; + } + + //------------------// + // checkInitialized // + //------------------// + /** + * Check the unit+name have been assigned to this constant object. + * They are mandatory to link the constant to the persistency mechanism. + */ + private void checkInitialized () + { + int i = 0; + + // Make sure everything is initialized properly + while (qualifiedName == null) { + i++; + UnitManager.getInstance().checkDirtySets(); + } + + // For monitoring/debugging only + if (i > 1) { + System.out.println( + "*** " + Thread.currentThread().getName() + + " checkInitialized loop:" + i); + } + } + + //----------// + // getTuple // + //----------// + /** + * Report the current tuple data, which may imply to trigger the + * assignment of qualified name to the constant, in order to get + * property data + * + * @return the current tuple data + */ + private Tuple getTuple () + { + checkInitialized(); + + return tuple.get(); + } + + //~ Inner Classes ---------------------------------------------------------- + //-------// + // Angle // + //-------// + /** + * A subclass of Double, meant to store an angle (in radians). + */ + public static class Angle + extends Constant.Double + { + //~ Constructors ------------------------------------------------------- + + /** + * Specific constructor, where 'unit' and 'name' are assigned later + * + * @param defaultValue the (double) default value + * @param description the semantic of the constant + */ + public Angle (double defaultValue, + java.lang.String description) + { + super("Radians", defaultValue, description); + } + } + + //---------// + // Boolean // + //---------// + /** + * A subclass of Constant, meant to store a boolean value. + */ + public static class Boolean + extends Constant + { + //~ Constructors ------------------------------------------------------- + + /** + * Specific constructor, where 'unit' and 'name' are assigned later + * + * @param defaultValue the (boolean) default value + * @param description the semantic of the constant + */ + public Boolean (boolean defaultValue, + java.lang.String description) + { + super(null, java.lang.Boolean.toString(defaultValue), description); + } + + //~ Methods ------------------------------------------------------------ + /** + * Retrieve the current constant value + * + * @return the current (boolean) value + */ + public boolean getValue () + { + return ((java.lang.Boolean) getCachedValue()).booleanValue(); + } + + /** + * Convenient method to access this boolean value + * + * @return true if set, false otherwise + */ + public boolean isSet () + { + return getValue(); + } + + /** + * Allows to set a new boolean value (passed as a string) to this + * constant. The string validity is actually checked. + * + * @param string the boolean value as a string + */ + @Override + public void setValue (java.lang.String string) + { + setValue(java.lang.Boolean.valueOf(string).booleanValue()); + } + + /** + * Set a new value to the constant + * + * @param val the new (boolean) value + */ + public void setValue (boolean val) + { + setTuple(java.lang.Boolean.toString(val), val); + } + + @Override + protected java.lang.Boolean decode (java.lang.String str) + { + return java.lang.Boolean.valueOf(str); + } + } + + //-------// + // Color // + //-------// + /** + * A subclass of Constant, meant to store a {@link java.awt.Color} + * value. + * They have a disk repository which is separate from the other constants. + */ + public static class Color + extends Constant + { + //~ Constructors ------------------------------------------------------- + + /** + * Normal constructor, with a String type for default value + * + * @param unit the enclosing unit + * @param name the constant name + * @param defaultValue the default (String) RGB value + * @param description the semantic of the constant + */ + public Color (java.lang.String unit, + java.lang.String name, + java.lang.String defaultValue, + java.lang.String description) + { + super(null, defaultValue, description); + setUnitAndName(unit, name); + } + + //~ Methods ------------------------------------------------------------ + //-------------// + // decodeColor // + //-------------// + public static java.awt.Color decodeColor (java.lang.String str) + { + return java.awt.Color.decode(str); + } + + //-------------// + // encodeColor // + //-------------// + public static java.lang.String encodeColor (java.awt.Color color) + { + return java.lang.String.format( + "#%02x%02x%02x", + color.getRed(), + color.getGreen(), + color.getBlue()); + } + + /** + * Retrieve the current constant value + * + * @return the current (Color) value + */ + public java.awt.Color getValue () + { + return (java.awt.Color) getCachedValue(); + } + + /** + * Allows to set a new int RGB value (passed as a string) to this + * constant. The string validity is actually checked. + * + * @param string the int value as a string + */ + @Override + public void setValue (java.lang.String string) + { + setValue(java.awt.Color.decode(string)); + } + + /** + * Set a new value to the constant + * + * @param val the new Color value + */ + public void setValue (java.awt.Color val) + { + setTuple(encodeColor(val), val); + } + + @Override + protected java.awt.Color decode (java.lang.String str) + { + return decodeColor(str); + } + } + + //--------// + // Double // + //--------// + /** + * A subclass of Constant, meant to store a double value. + */ + public static class Double + extends Constant + { + //~ Constructors ------------------------------------------------------- + + public Double (java.lang.String quantityUnit, + double defaultValue, + java.lang.String description) + { + super( + quantityUnit, + java.lang.Double.toString(defaultValue), + description); + } + + //~ Methods ------------------------------------------------------------ + public double getValue () + { + return ((DoubleValue) getCachedValue()).doubleValue(); + } + + public DoubleValue getWrappedValue () + { + // Return a copy + return new DoubleValue(getValue()); + } + + public void setValue (double val) + { + setTuple(java.lang.Double.toString(val), new DoubleValue(val)); + } + + public void setValue (DoubleValue val) + { + setTuple(val.toString(), val); + } + + @Override + public void setValue (java.lang.String string) + { + setValue(decode(string)); + } + + @Override + protected DoubleValue decode (java.lang.String str) + { + return new DoubleValue(java.lang.Double.valueOf(str)); + } + } + + //---------// + // Integer // + //---------// + /** + * A subclass of Constant, meant to store an int value. + */ + public static class Integer + extends Constant + { + //~ Constructors ------------------------------------------------------- + + /** + * Specific constructor, where 'unit' and 'name' are assigned later + * + * @param quantityUnit unit used by this value + * @param defaultValue the (int) default value + * @param description the semantic of the constant + */ + public Integer (java.lang.String quantityUnit, + int defaultValue, + java.lang.String description) + { + super( + quantityUnit, + java.lang.Integer.toString(defaultValue), + description); + } + + //~ Methods ------------------------------------------------------------ + /** + * Retrieve the current constant value + * + * @return the current (int) value + */ + public int getValue () + { + return (java.lang.Integer) getCachedValue(); + } + + /** + * Allows to set a new int value (passed as a string) to this + * constant. The string validity is actually checked. + * + * @param string the int value as a string + */ + @Override + public void setValue (java.lang.String string) + { + setValue(java.lang.Integer.valueOf(string).intValue()); + } + + /** + * Set a new value to the constant + * + * @param val the new (int) value + */ + public void setValue (int val) + { + setTuple(java.lang.Integer.toString(val), val); + } + + @Override + protected java.lang.Integer decode (java.lang.String str) + { + return new java.lang.Integer(str); + } + } + + //-------// + // Ratio // + //-------// + /** + * A subclass of Double, meant to store a ratio or percentage. + */ + public static class Ratio + extends Constant.Double + { + //~ Constructors ------------------------------------------------------- + + /** + * Specific constructor, where 'unit' and 'name' are assigned later + * + * @param defaultValue the (double) default value + * @param description the semantic of the constant + */ + public Ratio (double defaultValue, + java.lang.String description) + { + super(null, defaultValue, description); + } + } + + //--------// + // String // + //--------// + /** + * A subclass of Constant, meant to store a string value. + */ + public static class String + extends Constant + { + //~ Constructors ------------------------------------------------------- + + /** + * Normal constructor, with a string type for default value + * + * @param unit the enclosing unit + * @param name the constant name + * @param defaultValue the default (string) value + * @param description the semantic of the constant + */ + public String (java.lang.String unit, + java.lang.String name, + java.lang.String defaultValue, + java.lang.String description) + { + this(defaultValue, description); + setUnitAndName(unit, name); + } + + /** + * Specific constructor, where 'unit' and 'name' are assigned later + * + * @param defaultValue the (string) default value + * @param description the semantic of the constant + */ + public String (java.lang.String defaultValue, + java.lang.String description) + { + super(null, defaultValue, description); + } + + //~ Methods ------------------------------------------------------------ + /** + * Retrieve the current constant value. + * Actually this is synonymous with currentString() + * + * @return the current (string) value + */ + public java.lang.String getValue () + { + return (java.lang.String) getCachedValue(); + } + + /** + * Set a new string value to the constant + * + * @param val the new (string) value + */ + @Override + public void setValue (java.lang.String val) + { + setTuple(val, val); + } + + @Override + protected java.lang.String decode (java.lang.String str) + { + return str; + } + } + + //-------// + // Tuple // + //-------// + /** + * Class used to handle the tuple [currentString + currentValue] + * in an atomic way. + */ + private static class Tuple + { + //~ Instance fields ---------------------------------------------------- + + final java.lang.String currentString; + + final Object cachedValue; + + //~ Constructors ------------------------------------------------------- + public Tuple (java.lang.String currentString, + Object cachedValue) + { + /** Current string Value */ + this.currentString = currentString; + + /** Current cached Value (optimized) */ + this.cachedValue = cachedValue; + } + + //~ Methods ------------------------------------------------------------ + @Override + public java.lang.String toString () + { + return currentString; + } + } +} diff --git a/src/main/omr/constant/ConstantManager.java b/src/main/omr/constant/ConstantManager.java new file mode 100644 index 0000000..04a7925 --- /dev/null +++ b/src/main/omr/constant/ConstantManager.java @@ -0,0 +1,453 @@ +//----------------------------------------------------------------------------// +// // +// C o n s t a n t M a n a g e r // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.constant; + +import omr.Main; +import omr.WellKnowns; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import net.jcip.annotations.ThreadSafe; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Map.Entry; +import java.util.Properties; +import java.util.SortedSet; +import java.util.TreeSet; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Class {@code ConstantManager} manages the persistency of the whole + * population of Constants, including their mapping to properties, + * their storing on disk and their reloading from disk. + * + *

+ * The actual value of an application "constant", as returned by the method + * {@link Constant#getCurrentString}, is determined in the following order, any + * definition overriding the previous ones: + * + *

  1. First, SOURCE values are always provided within + * source declaration of the constants in the Java source file + * itself. + * For example, in the "omr/sheet/ScaleBuilder.java" file, + * we can find the following declaration which defines the minimum value for + * sheet resolution, here specified in pixels (the application has difficulties + * with scans of lower resolution). + * + *
    + * Constant.Integer minResolution = new Constant.Integer(
    + *    "Pixels",
    + *    11,
    + *    "Minimum resolution, expressed as number of pixels per interline");
    + * 
    This declaration must be read as follows:
      + * + *
    • {@code minResolution} is the Java object used in the application. + * It is defined as a Constant.Integer, a subtype of Constant meant to host + * Integer values
    • + * + *
    • {@code "Pixels"} specifies the unit used. Here we are counting in + * pixels.
    • + * + *
    • {@code 11} is the constant value. This is the value used by the + * application, provided it is not overridden in the USER properties file + * or later via a dedicated GUI tool.
    • + * + *
    • "Minimum resolution, expressed as number of pixels per interline" + * is the constant description, which will be used as a tool tip in + * the GUI interface in charge of editing these constants.
    + * + *

  2. + * + *
  3. Then, USER values, contained in a property file named + * "run.properties" can assign overriding values to some + * constants. For example, the {@code minInterline} constant + * above could be altered by the following line in this user file: + *
    + * omr.sheet.ScaleBuilder.minInterline=12
    + * This file is modified every time the user updates the value of a constant by + * means of the provided Constant user interface at run-time. + * The file is not mandatory, and is located in the user application data + * {@code config} folder. + * Its values override the SOURCE corresponding constants. + * Typically, these USER values represent some modification made by the end user + * at run-time and thus saved from one run to the other. + * The file is not meant to be edited manually, but rather through the provided + * GUI tool.
  4. + *
    + * + *
  5. Then, CLI values, as set on the command line interface, by means + * of the "-option" key=value command. For further details on + * this command, refer to the {@link omr.CLI} class documentation. + *
    Persistency here depends on the way Audiveris is running:
      + *
    • When running in batch mode, these CLI-defined constant values + * are not persisted in the USER file, unless the constant + * {@code omr.Main.persistBatchCliConstants} is set to true.
    • + *
    • When running in interactive mode, these CLI-defined constant + * values are always persisted in the USER file.

  6. + * + *
  7. Finally, UI Options Menu values, as set online through the + * graphical user interface. These constant values defined at the GUI level are + * persisted in the USER file.
+ * + *

The whole set of constant values is stored on disk when the application + * is closed. Doing so, the disk values are always kept in synch with the + * program values, provided the application is normally closed rather than + * killed. It can also be stored programmatically by calling the + * {@link #storeResource} method. + * + *

Only the USER property file is written, the SOURCE values in the source + * code are not altered. Moreover, if the user has modified a value in such a + * way that the final value is the same as in the source, the value is simply + * discarded from the USER property file. + * Doing so, the USER property file really contains only the additions of this + * particular user.

+ * + * @author Hervé Bitteur + */ +@ThreadSafe +public class ConstantManager +{ + //~ Static fields/initializers --------------------------------------------- + + /** Usual logger utility */ + private static final Logger logger = LoggerFactory.getLogger( + ConstantManager.class); + + /** User properties file name */ + private static final String USER_FILE_NAME = "run.properties"; + + /** The singleton */ + private static final ConstantManager INSTANCE = new ConstantManager(); + + //~ Instance fields -------------------------------------------------------- + /** + * Map of all constants created in the application, regardless whether these + * constants are enclosed in a ConstantSet or defined as standalone entities + */ + protected final ConcurrentHashMap constants = new ConcurrentHashMap<>(); + + /** User properties */ + private final UserHolder userHolder = new UserHolder( + new File(WellKnowns.CONFIG_FOLDER, USER_FILE_NAME)); + + //~ Constructors ----------------------------------------------------------- + //-----------------// + // ConstantManager // + //-----------------// + private ConstantManager () + { + } + + //~ Methods ---------------------------------------------------------------- + //------------------// + // getAllProperties // + //------------------// + /** + * Report the whole collection of properties (coming from USER + * sources) backed up on disk. + * + * @return the collection of constant properties + */ + public Collection getAllProperties () + { + SortedSet props = new TreeSet<>(userHolder.getKeys()); + + return props; + } + + //-------------// + // getInstance // + //-------------// + /** + * Report the singleton of this class. + * + * @return the only ConstantManager instance + */ + public static ConstantManager getInstance () + { + return INSTANCE; + } + + //-------------// + // addConstant // + //-------------// + /** + * Register a brand new constant with a provided name to retrieve + * a predefined value loaded from disk backup if any. + * + * @param qName the constant qualified name + * @param constant the Constant instance to register + * @return the loaded value if any, otherwise null + */ + public String addConstant (String qName, + Constant constant) + { + if (qName == null) { + throw new IllegalArgumentException( + "Attempt to add a constant with no qualified name"); + } + + Constant old = constants.putIfAbsent(qName, constant); + + if ((old != null) && (old != constant)) { + throw new IllegalArgumentException( + "Attempt to duplicate constant " + qName); + } + + // Value set at CLI level? + Properties cliConstants = Main.getCliConstants(); + + if (cliConstants != null) { + String cliValue = cliConstants.getProperty(qName); + + if (cliValue != null) { + return cliValue; + } + } + + // Fallback on using user value + return userHolder.getProperty(qName); + } + + //-------------------------// + // getUnusedUserProperties // + //-------------------------// + /** + * Report the collection of USER properties that do not relate to + * any known application Constant. + * + * @return the potential old stuff in USER properties + */ + public Collection getUnusedUserProperties () + { + return userHolder.getUnusedKeys(); + } + + //----------------// + // removeConstant // + //----------------// + /** + * Remove a constant. + * + * @param constant the constant to remove + * @return the removed Constant, or null if not found + */ + public Constant removeConstant (Constant constant) + { + if (constant.getQualifiedName() == null) { + throw new IllegalArgumentException( + "Attempt to remove a constant with no qualified name defined"); + } + + return constants.remove(constant.getQualifiedName()); + } + + //---------------// + // storeResource // + //---------------// + /** + * Stores the current content of the whole property set to disk. + * More specifically, only the values set OUTSIDE the original Default + * parts are stored, and they are stored in the user property file. + */ + public void storeResource () + { + userHolder.store(); + } + + //----------------------// + // getConstantUserValue // + //----------------------// + String getConstantUserValue (String qName) + { + return userHolder.getProperty(qName); + } + + //~ Inner Classes ---------------------------------------------------------- + //----------------// + // AbstractHolder // + //----------------// + private class AbstractHolder + { + //~ Instance fields ---------------------------------------------------- + + /** Related file */ + protected final File file; + + /** The handled properties */ + protected Properties properties; + + //~ Constructors ------------------------------------------------------- + public AbstractHolder (File file) + { + this.file = file; + } + + //~ Methods ------------------------------------------------------------ + public Collection getKeys () + { + Collection strings = new ArrayList<>(); + + for (Object obj : properties.keySet()) { + strings.add((String) obj); + } + + return strings; + } + + public String getProperty (String key) + { + return properties.getProperty(key); + } + + public Collection getUnusedKeys () + { + SortedSet props = new TreeSet<>(); + + for (Object obj : properties.keySet()) { + if (!constants.containsKey((String) obj)) { + props.add((String) obj); + } + } + + return props; + } + + public Collection getUselessKeys () + { + SortedSet props = new TreeSet<>(); + + for (Entry entry : properties.entrySet()) { + Constant constant = constants.get((String) entry.getKey()); + + if ((constant != null) + && constant.getSourceString().equals(entry.getValue())) { + props.add((String) entry.getKey()); + } + } + + return props; + } + + public void load () + { + // Load from local file + if (file != null) { + loadFromFile(); + } + } + + private void loadFromFile () + { + + + try (InputStream in = new FileInputStream(file)) { + properties.load(in); + } catch (FileNotFoundException ignored) { + // This is not at all an error + logger.debug("[{}" + "]" + " No property file {}", + ConstantManager.class.getName(), + file.getAbsolutePath()); + } catch (IOException ex) { + logger.error("Error loading constants file {}", + file.getAbsolutePath()); + } + } + } + + //------------// + // UserHolder // + //------------// + /** + * Triggers the loading of user property file. + * Any modification made at run-time will be saved in the user part. + */ + private class UserHolder + extends AbstractHolder + { + + //~ Constructors ------------------------------------------------------- + public UserHolder (File file) + { + super(file); + properties = new Properties(); + load(); + } + + //~ Methods ------------------------------------------------------------ + /** + * Remove from the USER collection the properties that are + * already in the source with identical value, + * and insert properties that need to reflect the current values + * which differ from source. + */ + public void cleanup () + { + // Browse all constant entries + for (Entry entry : constants.entrySet()) { + final String key = entry.getKey(); + final Constant constant = entry.getValue(); + + final String current = constant.getCurrentString(); + final String source = constant.getSourceString(); + + if (!current.equals(source)) { + logger.debug( + "Writing User value for key: {} = {}", + key, current); + + properties.setProperty(key, current); + } else { + if (properties.remove(key) != null) { + logger.debug( + "Removing User value for key: {} = {}", + key, current); + } + } + } + } + + public void store () + { + // First purge properties + cleanup(); + + // Then, save the remaining values + logger.debug("Store constants into {}", file); + + // First make sure the directory exists (Brenton patch) + if (file.getParentFile().mkdirs()) { + logger.info("Creating {}", file); + } + + // Then write down the properties + try (FileOutputStream out = new FileOutputStream(file)) { + properties.store(out, + " Audiveris user properties file. Do not edit"); + } catch (FileNotFoundException ex) { + logger.warn("Property file {} not found or not writable", + file.getAbsolutePath()); + } catch (IOException ex) { + logger.warn("Error while storing the property file {}", + file.getAbsolutePath()); + } + } + } +} diff --git a/src/main/omr/constant/ConstantSet.java b/src/main/omr/constant/ConstantSet.java new file mode 100644 index 0000000..8204532 --- /dev/null +++ b/src/main/omr/constant/ConstantSet.java @@ -0,0 +1,289 @@ +//----------------------------------------------------------------------------// +// // +// C o n s t a n t S e t // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.constant; + +import net.jcip.annotations.ThreadSafe; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.lang.reflect.Field; +import java.util.Collections; +import java.util.SortedMap; +import java.util.TreeMap; + +/** + * This abstract class handles a set of Constants as a whole. In particular, + * this allows a user interface (such as {@link UnitTreeTable}) to present an + * editing table of the whole set of constants. + * + *

We recommend to define only one such static ConstantSet per class/unit as + * a subclass of this (abstract) ConstantSet.

+ * + * @author Hervé Bitteur + */ +@ThreadSafe +public abstract class ConstantSet +{ + //~ Static fields/initializers --------------------------------------------- + + /** Usual logger utility */ + private static final Logger logger = LoggerFactory.getLogger(ConstantSet.class); + + //~ Instance fields -------------------------------------------------------- + /** Name of the containing unit/class */ + private final String unit; + + /** + * The mapping between constant name & constant object. We use a sorted map + * to allow access by constant index in constant set, as required by + * ConstantTreeTable. This instance can only be lazily constructed, thanks + * to {@link #getMap} method, since all the enclosed constants must have + * been constructed beforehand. + */ + private volatile SortedMap map; + + //~ Constructors ----------------------------------------------------------- + //-------------// + // ConstantSet // + //-------------// + /** + * A new ConstantSet instance is created, and registered at the UnitManager + * singleton, but its map of internal constants will need to be built later. + */ + public ConstantSet () + { + unit = getClass().getDeclaringClass().getName(); + + // System.out.println( + // "\n" + Thread.currentThread().getName() + + // ": Creating ConstantSet " + unit); + + // Register this instance + UnitManager.getInstance().addSet(this); + } + + //~ Methods ---------------------------------------------------------------- + //--------// + // dumpOf // + //--------// + /** + * A utility method to dump current value of each constant in the set. + * + * @return the string representation of this set + */ + public String dumpOf () + { + StringBuilder sb = new StringBuilder(); + sb.append(String.format("[%s]%n", unit)); + + for (Constant constant : getMap().values()) { + String origin = constant.getValueOrigin(); + if (origin.equals("SRC")) { + origin = ""; + } else { + origin = "[" + origin + "]"; + } + sb.append(String.format( + "%-25s %12s %-14s =%5s %-25s\t%s%n", + constant.getName(), + constant.getShortTypeName(), + (constant.getQuantityUnit() != null) + ? ("(" + constant.getQuantityUnit() + ")") : "", + origin, + constant.getCurrentString(), + constant.getDescription())); + } + + return sb.toString(); + } + + //-------------// + // getConstant // + //-------------// + /** + * Report a constant knowing its name in the constant set + * + * @param name the desired name + * + * @return the proper constant, or null if not found + */ + public Constant getConstant (String name) + { + return getMap().get(name); + } + + //-------------// + // getConstant // + //-------------// + /** + * Report a constant knowing its index in the constant set + * + * @param i the desired index value + * + * @return the proper constant + */ + public Constant getConstant (int i) + { + return Collections.list(Collections.enumeration(getMap().values())).get( + i); + } + + //---------// + // getName // + //---------// + /** + * Report the name of the enclosing unit + * + * @return unit name + */ + public String getName () + { + return unit; + } + + //------------// + // initialize // + //------------// + /** + * Make sure this ConstantSet has properly been initialized (its map of + * constants has been built) + * + * @return true if initialized correctly, false otherwise + */ + public boolean initialize () + { + return getMap() != null; + } + + //------------// + // isModified // + //------------// + /** + * Predicate to check whether at least one of the constant of the set has + * been modified + * + * @return the modification status of the whole set + */ + public boolean isModified () + { + for (Constant constant : getMap().values()) { + if (constant.isModified()) { + return true; + } + } + + return false; + } + + //------// + // size // + //------// + /** + * Report the number of constants in this constant set + * + * @return the size of the constant set + */ + public int size () + { + return getMap().size(); + } + + //----------// + // toString // + //----------// + /** + * Return the last part of the ConstantSet name, without the leading package + * names. This short name is used by Constant TreeTable + * + * @return just the (unqualified) name of the ConstantSet + */ + @Override + public String toString () + { + StringBuilder sb = new StringBuilder(); + + //sb.append(""); + int dot = unit.lastIndexOf('.'); + + if (dot != -1) { + sb.append(unit.substring(dot + 1)); + } else { + sb.append(unit); + } + + //sb.append(""); + return sb.toString(); + } + + //--------// + // getMap // + //--------// + private SortedMap getMap () + { + if (map == null) { + // Initialize map content + initMap(); + } + + return map; + } + + //---------// + // initMap // + //---------// + /** + * Now that the enclosed constants of this set have been constructed, let + * assign them their unit and name parameters. + */ + private void initMap () + { + SortedMap tempMap = new TreeMap<>(); + + // Retrieve values of all fields + Class cl = getClass(); + + try { + for (Field field : cl.getDeclaredFields()) { + field.setAccessible(true); + + String name = field.getName(); + + // Make sure that we have only Constants in this ConstantSet + Object obj = field.get(this); + + // Not yet allocated, no big deal, we'll get back to it later + if (obj == null) { + ///logger.warn("ConstantSet not fully allocated yet"); + return; + } + + if (obj instanceof Constant) { + Constant constant = (Constant) obj; + constant.setUnitAndName(unit, name); + tempMap.put(name, constant); + } else { + logger.error( + "ConstantSet in unit ''{}'' contains a non" + + " Constant field ''{}'' obj= {}", + unit, name, obj); + } + } + + // Assign the constructed map atomically + map = tempMap; + } catch (SecurityException | IllegalArgumentException | + IllegalAccessException ex) { + logger.warn("Error initializing map of ConstantSet " + this, ex); + } + } +} diff --git a/src/main/omr/constant/Node.java b/src/main/omr/constant/Node.java new file mode 100644 index 0000000..3fce692 --- /dev/null +++ b/src/main/omr/constant/Node.java @@ -0,0 +1,103 @@ +//----------------------------------------------------------------------------// +// // +// N o d e // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.constant; + +import java.util.Comparator; + +/** + * Abstract class {@code Node} represents a node in the hierarchy of + * packages and units (aka classes). + * + * @author Hervé Bitteur + */ +public abstract class Node +{ + //~ Static fields/initializers --------------------------------------------- + + /** For comparing Node instances according to their name */ + public static final Comparator nameComparator = new Comparator() { + @Override + public int compare (Node n1, + Node n2) + { + return n1.getName() + .compareTo(n2.getName()); + } + }; + + + //~ Instance fields -------------------------------------------------------- + + /** (Fully qualified) name of the node */ + private final String name; + + //~ Constructors ----------------------------------------------------------- + + //------// + // Node // + //------// + /** + * Create a new Node. + * @param name the fully qualified node name + */ + public Node (String name) + { + this.name = name; + } + + //~ Methods ---------------------------------------------------------------- + + //---------// + // getName // + //---------// + /** + * Report the fully qualified name for this node. + * @return the fully qualified node name + */ + public String getName () + { + return name; + } + + //---------------// + // getSimpleName // + //---------------// + /** + * Return the last path component (non-qualified). + * @return the non-qualified node name + */ + public String getSimpleName () + { + int dot = name.lastIndexOf('.'); + + if (dot != -1) { + return name.substring(dot + 1); + } else { + return name; + } + } + + //----------// + // toString // + //----------// + /** + * Since {@code toString()} is used by JTreeTable to display the + * node name, this method returns the last path component of the + * node, in other words the non-qualified name. + * @return the non-qualified node name + */ + @Override + public String toString () + { + return getSimpleName(); + } +} diff --git a/src/main/omr/constant/PackageNode.java b/src/main/omr/constant/PackageNode.java new file mode 100644 index 0000000..897ba76 --- /dev/null +++ b/src/main/omr/constant/PackageNode.java @@ -0,0 +1,100 @@ +//----------------------------------------------------------------------------// +// // +// P a c k a g e N o d e // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.constant; + +import java.util.concurrent.ConcurrentSkipListSet; + +/** + * Class {@code PackageNode} represents a package in the hierarchy of + * nodes. It can have children, which can be sub-packages and units. For + * example, the unit/class omr.score.Page will need PackageNode + * omr and PackageNode omr.score. + * + * @author Hervé Bitteur + */ +public class PackageNode + extends Node +{ + //~ Instance fields -------------------------------------------------------- + + /** + * The children, composed of either other {@code PackageNode} or + * {@code ConstantSet}. + */ + private final ConcurrentSkipListSet children = new ConcurrentSkipListSet<>( + Node.nameComparator); + + //~ Constructors ----------------------------------------------------------- + + //-------------// + // PackageNode // + //-------------// + /** + * Create a new PackageNode. + * + * @param name the fully qualified package name + * @param child the first child of the package (either a sub-package, or a + * ConstantSet). + */ + public PackageNode (String name, + Node child) + { + super(name); + + if (child != null) { + addChild(child); + } + } + + //~ Methods ---------------------------------------------------------------- + + //----------// + // addChild // + //----------// + /** + * Add a child to the package children + * + * @param obj the child to add (sub-package or ConstantSet) + */ + public final void addChild (Node obj) + { + children.add(obj); + } + + //----------// + // getChild // + //----------// + /** + * Return the child at given index + * + * @param index the position in the ordered children list + * + * @return the desired child + */ + public Object getChild (int index) + { + return children.toArray()[index]; + } + + //---------------// + // getChildCount // + //---------------// + /** + * Return the number of children currently in this package node + * + * @return the count of children + */ + public int getChildCount () + { + return children.size(); + } +} diff --git a/src/main/omr/constant/UnitManager.java b/src/main/omr/constant/UnitManager.java new file mode 100644 index 0000000..5d480bd --- /dev/null +++ b/src/main/omr/constant/UnitManager.java @@ -0,0 +1,525 @@ +//----------------------------------------------------------------------------// +// // +// U n i t M a n a g e r // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.constant; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import net.jcip.annotations.ThreadSafe; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Iterator; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Locale; +import java.util.Set; +import java.util.SortedSet; +import java.util.TreeSet; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentSkipListSet; + +/** + * Class {@code UnitManager} manages all units (aka classes), + * for which we have a ConstantSet. + * + *

To help {@link UnitTreeTable} display the whole tree of UnitNodes, + * UnitManager can pre-load all the classes known to contain a ConstantSet. + * This list is kept up-to-date and stored as a property. + * + * @author Hervé Bitteur + */ +@ThreadSafe +public class UnitManager +{ + //~ Static fields/initializers --------------------------------------------- + + /** Name of this unit */ + private static final String UNIT = UnitManager.class.getName(); + + /** The single instance of this class */ + private static final UnitManager INSTANCE = new UnitManager(); + + /** Separator used in property that concatenates all unit names */ + private static final String SEPARATOR = ";"; + + /** Usual logger utility */ + private static final Logger logger = LoggerFactory.getLogger(UnitManager.class); + + //~ Instance fields -------------------------------------------------------- + // + /** The root node. */ + private final PackageNode root = new PackageNode("", null); + + /** Map of PackageNodes and UnitNodes. */ + private final ConcurrentHashMap mapOfNodes = new ConcurrentHashMap<>(); + + /** Set of names of ConstantSets that still need to be initialized. */ + private final ConcurrentSkipListSet dirtySets = new ConcurrentSkipListSet<>(); + + /** + * Lists of all units known as containing a constantset. + * This is kept up-to-date and saved as a property. + */ + private Constant.String units; + + /** Flag to avoid storing units being pre-loaded. */ + private volatile boolean storeIt = false; + + //~ Constructors ----------------------------------------------------------- + //-------------// + // UnitManager // + //-------------// + /** This is a singleton. */ + private UnitManager () + { + mapOfNodes.put("", root); + } + + //~ Methods ---------------------------------------------------------------- + //-------------// + // getInstance // + //-------------// + /** + * Report the single instance of this package. + * + * @return the single instance + */ + public static UnitManager getInstance () + { + return INSTANCE; + } + + //--------// + // addSet // + //--------// + /** + * Add a ConstantSet, which means perhaps adding a UnitNode if not + * already allocated and setting its ConstantSet reference to the + * provided ConstantSet. + * + * @param set the ConstantSet to add to the hierarchy + */ + public void addSet (ConstantSet set) + { + ///log ("addSet set=" + set.getName()); + retrieveUnit(set.getName()).setConstantSet(set); + + // Register this name in the dirty ones + dirtySets.add(set.getName()); + } + + //---------------// + // checkAllUnits // + //---------------// + /** + * Check if all defined constants are used by at least one unit. + */ + public void checkAllUnits () + { + SortedSet constants = new TreeSet<>(); + + for (Node node : mapOfNodes.values()) { + if (node instanceof UnitNode) { + UnitNode unit = (UnitNode) node; + ConstantSet set = unit.getConstantSet(); + + if (set != null) { + for (int i = 0; i < set.size(); i++) { + Constant constant = set.getConstant(i); + constants.add( + unit.getName() + "." + constant.getName()); + } + } + } + } + + dumpStrings("constants", constants); + + Collection props = ConstantManager.getInstance(). + getAllProperties(); + props.removeAll(constants); + dumpStrings("Non set-enclosed properties", props); + + dumpStrings( + "Unused User properties", + ConstantManager.getInstance().getUnusedUserProperties()); + } + + //----------------// + // checkDirtySets // + //----------------// + /** + * Go through all registered to-be-initialized sets, and initialize + * them, then clear the set of such dirty sets. + */ + public void checkDirtySets () + { + int rookies = 0; + + // We use (and clear) the collection of rookies + for (Iterator it = dirtySets.iterator(); it.hasNext();) { + String name = it.next(); + rookies++; + + // System.out.println( + // Thread.currentThread().getName() + ": checkDirtySets. name=" + + // name); + UnitNode unit = (UnitNode) getNode(name); + + if (unit.getConstantSet().initialize()) { + it.remove(); + } + } + + // System.out.println( + // Thread.currentThread().getName() + ": checkDirtySets. rookies:" + + // rookies); + } + + //--------------// + // dumpAllUnits // + //--------------// + /** + * Dumps on the standard output the current value of all Constants + * of all ConstantSets. + */ + public void dumpAllUnits () + { + StringBuilder sb = new StringBuilder(); + sb.append(String.format("UnitManager. All Units:%n")); + + // Use alphabetical order for easier reading + List nodes = new ArrayList<>(mapOfNodes.values()); + Collections.sort(nodes, Node.nameComparator); + + for (Node node : nodes) { + if (node instanceof UnitNode) { + UnitNode unit = (UnitNode) node; + + // ConstantSet? + ConstantSet set = unit.getConstantSet(); + + if (set != null) { + sb.append(String.format("%n%s", set.dumpOf())); + } + } + } + + logger.info(sb.toString()); + } + + //---------// + // getNode // + //---------// + /** + * Retrieves a node object, knowing its path name. + * + * @param path fully qualified node name + * @return the node object, or null if not found + */ + public Node getNode (String path) + { + return mapOfNodes.get(path); + } + + //---------// + // getRoot // + //---------// + /** + * Return the PackageNode at the root of the node hierarchy. + * + * @return the root PackageNode + */ + public PackageNode getRoot () + { + return root; + } + + //--------------// + // preLoadUnits // + //--------------// + /** + * Allows to pre-load the names of the various nodes in the + * hierarchy, by simply extracting names stored at previous runs. + * This will load the classes not already loaded. + * This method is meant to be used by the UI which let the user browse and + * modify the whole collection of constants. + * + * @param main the application main class name + */ + public void preLoadUnits (String main) + { + //log("pre-loading units"); + String unitName; + + if (main != null) { + unitName = main + ".Units"; + } else { + unitName = "Units"; + } + + units = new Constant.String( + UNIT, + unitName, + "", + "List of units known as containing a ConstantSet"); + + // Initialize units using the constant 'units' + final String[] tokens = units.getValue().split(SEPARATOR); + + storeIt = false; + + for (String unit : tokens) { + try { + ///System.out.println ("pre-loading '" + unit + "'..."); + Class.forName(unit); // This loads its ConstantSet + //log ("unit '" + unit + "' pre-loaded"); + } catch (ClassNotFoundException ex) { + System.err.println( + "*** Cannot load ConstantSet " + unit + " " + ex); + } + } + + storeIt = true; + + // Save the latest set of Units + storeUnits(); + + //log("all units have been pre-loaded from " + main); + } + + //-------------// + // searchUnits // + //-------------// + /** + * Search for all the units for which the provided string is found + * in the unit name or the unit description. + * + * @param string the string to search for + * @return the set (perhaps empty) of the matching units, a mix of UnitNode + * and Constant instances. + */ + public Set searchUnits (String string) + { + String str = string.toLowerCase(Locale.ENGLISH); + Set found = new LinkedHashSet<>(); + + for (Node node : mapOfNodes.values()) { + if (node instanceof UnitNode) { + UnitNode unit = (UnitNode) node; + + // Search in unit name itself + if (unit.getSimpleName().toLowerCase(Locale.ENGLISH).contains( + str)) { + found.add(unit); + } + + // Search in unit constants, if any + ConstantSet set = unit.getConstantSet(); + + if (set != null) { + for (int i = 0; i < set.size(); i++) { + Constant constant = set.getConstant(i); + + if (constant.getName().toLowerCase().contains(str) + || constant.getDescription().toLowerCase(). + contains(str)) { + found.add(constant); + } + } + } + } + } + + return found; + } + + //---------// + // addUnit // + //---------// + /** + * Include a Unit in the hierarchy. + * + * @param unit the Unit to include + */ + private void addUnit (UnitNode unit) + { + //log ("addUnit unit=" + unit.getName()); + // Update the hierarchy. Include it in the map, as well as all needed + // intermediate package nodes if any is needed. + String name = unit.getName(); + + // Add this node and its parents as needed + if (mapOfNodes.putIfAbsent(name, unit) == null) { + updateParents(unit); + + if (storeIt) { + // Make this unit name permanent + storeUnits(); + } + } + } + + //-------------// + // dumpStrings // + //-------------// + private void dumpStrings (String title, + Collection strings) + { + StringBuilder sb = new StringBuilder(); + + sb.append(String.format("%s:%n", title)); + + for (String string : strings) { + sb.append(String.format("%s%n", string)); + } + + logger.info(sb.toString()); + } + + //--------------// + // retrieveUnit // + //--------------// + /** + * Looks for the unit with given name. + * If the unit does not exist, it is created and inserted in the hierarchy. + * + * @param name the name of the desired unit + * @return the unit (found, or created) + */ + private UnitNode retrieveUnit (String name) + { + Node node = getNode(name); + + if (node == null) { + // Create a hosting unit node + UnitNode unit = new UnitNode(name); + addUnit(unit); + + return unit; + } else if (node instanceof UnitNode) { + return (UnitNode) node; + } else if (node instanceof PackageNode) { + logger.error("Unit with same name as package {}", name); + } + + return null; + } + + //------------// + // storeUnits // + //------------// + /** + * Build a string by concatenating all node names and store it to + * disk for subsequent runs. + */ + private void storeUnits () + { + //log("storing units"); + + // Update the constant 'units' according to current units content + StringBuilder buf = new StringBuilder(1024); + + for (String name : mapOfNodes.keySet()) { + Node node = getNode(name); + + if (node instanceof UnitNode) { + if (buf.length() > 0) { + buf.append(SEPARATOR); + } + + buf.append(name); + } + } + + // Side-effect: all constants are stored to disk + units.setValue(buf.toString()); + + //log(units.getName() + "=" + units.getValue()); + } + + //---------------// + // updateParents // + //---------------// + /** + * Update the chain of parents of a Unit, by walking up all the + * package names found in the fully qualified Unit name, + * creating PackageNodes when needed, or adding a new child to an + * existing PackageNode. + * + * @param unit the Unit whose chain of parents is to be updated + */ + private void updateParents (UnitNode unit) + { + String name = unit.getName(); + int length = name.length(); + Node child = unit; + + for (int i = name.lastIndexOf('.', length - 1); i >= 0; + i = name.lastIndexOf('.', i - 1)) { + String parent = name.substring(0, i); + Node obj = mapOfNodes.get(parent); + + // Create a provision node for a future parent. + if (obj == null) { + //log("No parent " + sub + " found. Creating PackageNode."); + PackageNode pn = new PackageNode(parent, child); + + if (mapOfNodes.putIfAbsent(parent, pn) != null) { + // Already done by someone else, give up + return; + } + + child = pn; + } else if (obj instanceof PackageNode) { + //log("PackageNode " + sub + " found. adding child"); + ((PackageNode) obj).addChild(child); + + return; + } else { + Exception e = new IllegalStateException( + "unexpected node type " + obj.getClass() + " in map."); + e.printStackTrace(); + + return; + } + } + + // No intermediate parent found, so hook it to the root itself + getRoot().addChild(child); + } + + //---------------// + // resetAllUnits // + //---------------// + /** + * Reset all constants to their factory (source) value. + */ + public void resetAllUnits () + { + for (Node node : mapOfNodes.values()) { + if (node instanceof UnitNode) { + UnitNode unit = (UnitNode) node; + ConstantSet set = unit.getConstantSet(); + + if (set != null) { + for (int i = 0; i < set.size(); i++) { + Constant constant = set.getConstant(i); + constant.reset(); + } + } + } + } + } +} diff --git a/src/main/omr/constant/UnitModel.java b/src/main/omr/constant/UnitModel.java new file mode 100644 index 0000000..bf2a2c4 --- /dev/null +++ b/src/main/omr/constant/UnitModel.java @@ -0,0 +1,498 @@ +//----------------------------------------------------------------------------// +// // +// U n i t M o d e l // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.constant; + +import omr.sheet.Scale; +import omr.sheet.Sheet; +import omr.sheet.ui.SheetsController; + +import omr.ui.treetable.AbstractTreeTableModel; +import omr.ui.treetable.TreeTableModel; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.swing.JOptionPane; + +/** + * Class {@code UnitModel} implements a data model for units suitable + * for use in a JTreeTable. + * + *

A row in the UnitModel can be any instance of the 3 following types: + *

    + * + *
  • PackageNode to represent a package. Its children rows can be + * either (sub) PackageNodes or UnitNodes.
  • + * + *
  • UnitNode to represent a class that contains a ConstantSet. + * Its parent node is a PackageNode. Its children rows (if any) + * are the Constants of its ConstantSet.
  • + * + *
  • Constant to represent a constant within a ConstantSet. In that + * case, its parent node is a UnitNode. It has no children rows.
  • + *
+ * + * @author Hervé Bitteur + */ +public class UnitModel + extends AbstractTreeTableModel +{ + //~ Static fields/initializers --------------------------------------------- + + /** Usual logger utility */ + private static final Logger logger = LoggerFactory.getLogger( + UnitModel.class); + + //~ Enumerations ----------------------------------------------------------- + /** + * Enumeration type to describe each column of the JTreeTable + */ + public static enum Column + { + //~ Enumeration constant initializers ---------------------------------- + + /** + * The left column, assigned to tree structure, allows expansion + * and collapsing of sub-tree portions. + */ + TREE("Unit", true, 280, TreeTableModel.class), + /** + * Editable column with modification flag if node is a constant. + * Empty if node is a package. + */ + MODIF("Modif", true, 50, String.class), + /** + * Column that recalls the constant type, and thus the possible + * range of values. + */ + TYPE("Type", false, 70, String.class), + /** + * Column for the units, if any, used for the value. + */ + UNIT("Unit", false, 70, String.class), + /** + * Column relevant only for constants which are fractions of + * interline, as defined by {@link omr.sheet.Scale.Fraction}. + * The equivalent number of pixels is displayed, according to the scale + * of the currently selected Sheet. + * If there is no current Sheet, then just a question mark (?) is shown + */ + PIXEL("Pixels", false, 30, String.class), + /** + * Editable column for constant current value, with related tool + * tip retrieved from the constant declaration. + */ + VALUE("Value", true, 100, String.class), + /** + * Column dedicated to constant description. + */ + DESC("Description", false, 350, String.class); + //~ Instance fields ---------------------------------------------------- + + /** Java class to handle column content. */ + private final Class type; + + /** Is this column user editable?. */ + private final boolean editable; + + /** Header for the column. */ + private final String header; + + /** Width for column display. */ + private final int width; + + //~ Constructors ------------------------------------------------------- + //--------// + // Column // + //--------// + Column (String header, + boolean editable, + int width, + Class type) + { + this.header = header; + this.editable = editable; + this.width = width; + this.type = type; + } + + //~ Methods ------------------------------------------------------------ + //----------// + // getWidth // + //----------// + public int getWidth () + { + return width; + } + } + + //~ Constructors ----------------------------------------------------------- + //-----------// + // UnitModel // + //-----------// + /** + * Builds the model. + */ + public UnitModel () + { + super(UnitManager.getInstance().getRoot()); + } + + //~ Methods ---------------------------------------------------------------- + //----------// + // getChild // + //----------// + /** + * Returns the child of {@code parent} at index {@code index} in + * the parent's child array. + * + * @param parent a node in the tree, obtained from this data source + * @param i the child index in parent sequence + * @return the child of {@code parent at index index} + */ + @Override + public Object getChild (Object parent, + int i) + { + if (parent instanceof PackageNode) { + PackageNode pNode = (PackageNode) parent; + + return pNode.getChild(i); + } + + if (parent instanceof UnitNode) { + UnitNode unit = (UnitNode) parent; + ConstantSet set = unit.getConstantSet(); + + if (set != null) { + return set.getConstant(i); + } + } + + System.err.println( + "*** getChild. Unexpected node " + parent + ", type=" + + parent.getClass().getName()); + + return null; + } + + //---------------// + // getChildCount // + //---------------// + /** + * Returns the number of children of {@code parent}. + * + * @param parent a node in the tree, obtained from this data source + * @return the number of children of the node {@code parent} + */ + @Override + public int getChildCount (Object parent) + { + if (parent instanceof PackageNode) { + return ((PackageNode) parent).getChildCount(); + } + + if (parent instanceof UnitNode) { + UnitNode unit = (UnitNode) parent; + ConstantSet set = unit.getConstantSet(); + + return set.size(); + } + + if (parent instanceof Constant) { + return 0; + } + + System.err.println( + "*** getChildCount. Unexpected node " + parent + ", type=" + + parent.getClass().getName()); + + return 0; + } + + //----------------// + // getColumnClass // + //----------------// + /** + * Report the class for instances in the provided column. + * + * @param column the desired column + * @return the class for all cells in this column + */ + @Override + public Class getColumnClass (int column) + { + return Column.values()[column].type; + } + + //----------------// + // getColumnCount // + //----------------// + /** + * Report the number of column in the table. + * + * @return the table number of columns + */ + @Override + public int getColumnCount () + { + return Column.values().length; + } + + //---------------// + // getColumnName // + //---------------// + /** + * Report the name of the column at hand. + * + * @param column the desired column + * @return the column name + */ + @Override + public String getColumnName (int column) + { + return Column.values()[column].header; + } + + //------------// + // getValueAt // + //------------// + /** + * Report the value of a cell. + * + * @param node the desired node + * @param col the related column + * @return the cell value + */ + @Override + public Object getValueAt (Object node, + int col) + { + Column column = Column.values()[col]; + + switch (column) { + case MODIF: + + if (node instanceof PackageNode) { + return null; + } else if (node instanceof Constant) { + Constant constant = (Constant) node; + + return Boolean.valueOf(!constant.isSourceValue()); + } + + return ""; + + case VALUE: + + if (node instanceof Constant) { + Constant constant = (Constant) node; + + if (constant instanceof Constant.Boolean) { + Constant.Boolean cb = (Constant.Boolean) constant; + + return cb.getCachedValue(); + } else { + return constant.getCurrentString(); + } + } else { + return ""; + } + + case TYPE: + + if (node instanceof Constant) { + Constant constant = (Constant) node; + + return constant.getShortTypeName(); + } else { + return ""; + } + + case UNIT: + + if (node instanceof Constant) { + Constant constant = (Constant) node; + + return (constant.getQuantityUnit() != null) + ? constant.getQuantityUnit() : ""; + } else { + return ""; + } + + case PIXEL: + + if (node instanceof Constant) { + Constant constant = (Constant) node; + + if (constant instanceof Scale.Fraction + || constant instanceof Scale.LineFraction + || constant instanceof Scale.AreaFraction) { + // Compute the equivalent in pixels of this interline-based + // fraction, line or area fraction, provided that we have a + // current sheet and its scale is available. + Sheet sheet = SheetsController.getCurrentSheet(); + + if (sheet != null) { + Scale scale = sheet.getScale(); + + if (scale != null) { + if (constant instanceof Scale.Fraction) { + return Integer.valueOf( + scale.toPixels((Scale.Fraction) constant)); + } else if (constant instanceof Scale.LineFraction) { + return Integer.valueOf( + scale.toPixels( + (Scale.LineFraction) constant)); + } else if (constant instanceof Scale.AreaFraction) { + return Integer.valueOf( + scale.toPixels( + (Scale.AreaFraction) constant)); + } + } + } else { + return "?"; // Cannot compute the value + } + } + } + + return ""; + + case DESC: + + if (node instanceof Constant) { + Constant constant = (Constant) node; + + return constant.getDescription(); + } else { + return null; + } + } + + return null; // For the compiler + } + + //----------------// + // isCellEditable // + //----------------// + /** + * Predicate on cell being editable + * + * @param node the related tree node + * @param column the related table column + * @return true if editable + */ + @Override + public boolean isCellEditable (Object node, + int column) + { + Column col = Column.values()[column]; + + if (col == Column.MODIF) { + if (node instanceof Constant) { + Constant constant = (Constant) node; + + return Boolean.valueOf(!constant.isSourceValue()); + + // } else if (node instanceof UnitNode) { + // return true; + } else { + return false; + } + } else { + return col.editable; + } + } + + //--------// + // isLeaf // + //--------// + /** + * Returns {@code true} if {@code node} is a leaf. + * + * @param node a node in the tree, obtained from this data source + * @return true if {@code node} is a leaf + */ + @Override + public boolean isLeaf (Object node) + { + if (node instanceof Constant) { + return true; + } + + if (node instanceof UnitNode) { + UnitNode unit = (UnitNode) node; + + return (unit.getConstantSet() == null); + } + + return false; // By default + } + + //------------// + // setValueAt // + //------------// + /** + * Assign a value to a cell + * + * @param value the value to assign + * @param node the target node + * @param col the related column + */ + @Override + public void setValueAt (Object value, + Object node, + int col) + { + if (node instanceof Constant) { + Constant constant = (Constant) node; + Column column = Column.values()[col]; + + switch (column) { + case VALUE: + + try { + constant.setValue(value.toString()); + + // Forward modif to the modif status column and to the pixel + // column (brute force!) + fireTreeNodesChanged( + this, + new Object[]{getRoot()}, + null, + null); + } catch (NumberFormatException ex) { + JOptionPane.showMessageDialog( + null, + "Illegal number format"); + } + + break; + + case MODIF: + + if (!((Boolean) value).booleanValue()) { + constant.reset(); + fireTreeNodesChanged( + this, + new Object[]{getRoot()}, + null, + null); + } + + break; + + default: + } + } + } +} diff --git a/src/main/omr/constant/UnitNode.java b/src/main/omr/constant/UnitNode.java new file mode 100644 index 0000000..d0340f3 --- /dev/null +++ b/src/main/omr/constant/UnitNode.java @@ -0,0 +1,96 @@ +//----------------------------------------------------------------------------// +// // +// U n i t N o d e // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.constant; + +import org.slf4j.Logger; +/** + * Class {@code UnitNode} represents a unit (class) in the hierarchy of + * nodes. + * It represents a class and can have either a Logger, a ConstantSet, or both. + * + * @author Hervé Bitteur + */ +public class UnitNode + extends Node +{ + //~ Instance fields -------------------------------------------------------- + + /** The contained Constant set if any */ + private ConstantSet set; + + /** The logger if any */ + private Logger logger; + + //~ Constructors ----------------------------------------------------------- + + //----------// + // UnitNode // + //----------// + /** + * Create a new UnitNode. + * @param name the fully qualified class/unit name + */ + public UnitNode (String name) + { + super(name); + } + + //~ Methods ---------------------------------------------------------------- + + //----------------// + // getConstantSet // + //----------------// + /** + * Retrieves the ConstantSet associated to the unit (if any). + * @return the ConstantSet instance, or null + */ + public ConstantSet getConstantSet () + { + return set; + } + + //-----------// + // getLogger // + //-----------// + /** + * Retrieves the Logger instance associated to the unit (if any). + * @return the Logger instance, or null + */ + public Logger getLogger () + { + return logger; + } + + //----------------// + // setConstantSet // + //----------------// + /** + * Assigns the provided ConstantSet to this enclosing unit. + * @param set the ConstantSet to be assigned + */ + public void setConstantSet (ConstantSet set) + { + this.set = set; + } + + //-----------// + // setLogger // + //-----------// + /** + * Assigns the provided Logger to the unit. + * @param logger the Logger instance + */ + public void setLogger (Logger logger) + { + this.logger = logger; + } +} diff --git a/src/main/omr/constant/UnitTreeTable.java b/src/main/omr/constant/UnitTreeTable.java new file mode 100644 index 0000000..fc1736a --- /dev/null +++ b/src/main/omr/constant/UnitTreeTable.java @@ -0,0 +1,392 @@ +//----------------------------------------------------------------------------// +// // +// U n i t T r e e T a b l e // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.constant; + +import omr.ui.treetable.JTreeTable; +import omr.ui.treetable.TreeTableModelAdapter; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.awt.Color; +import java.awt.Component; +import java.awt.Font; +import java.awt.Rectangle; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +import javax.swing.JComponent; +import javax.swing.JTable; +import javax.swing.SwingConstants; +import javax.swing.UIManager; +import javax.swing.table.DefaultTableCellRenderer; +import javax.swing.table.TableCellEditor; +import javax.swing.table.TableCellRenderer; +import javax.swing.table.TableColumnModel; +import javax.swing.tree.TreePath; + +/** + * Class {@code UnitTreeTable} is a user interface that combines a tree + * to display the hierarchy of Units, that contains ConstantSets, + * and a table to display and edit the various Constants in + * each ConstantSet. + * + * @author Hervé Bitteur + */ +public class UnitTreeTable + extends JTreeTable +{ + //~ Static fields/initializers --------------------------------------------- + + /** Usual logger utility */ + private static final Logger logger = LoggerFactory.getLogger(UnitTreeTable.class); + + /** Alternate color for zebra appearance */ + private static final Color zebraColor = new Color(248, 248, 255); + + //~ Instance fields -------------------------------------------------------- + private TableCellRenderer valueRenderer = new ValueRenderer(); + + private TableCellRenderer pixelRenderer = new PixelRenderer(); + + //~ Constructors ----------------------------------------------------------- + //---------------// + // UnitTreeTable // + //---------------// + /** + * Create a User Interface JTreeTable dedicated to the handling of + * unit constants. + * + * @param model the corresponding data model + */ + public UnitTreeTable (UnitModel model) + { + super(model); + + setAutoResizeMode(JTable.AUTO_RESIZE_OFF); + + // Zebra + UIManager.put("Table.alternateRowColor", zebraColor); + setFillsViewportHeight(true); + + // Show grid? + //setShowGrid(true); + + // Specify column widths + adjustColumns(); + + // Customize the tree aspect + tree.setRootVisible(false); + tree.setShowsRootHandles(true); + + // Pre-expand all package nodes + preExpandPackages(); + } + + //~ Methods ---------------------------------------------------------------- + //---------------// + // getCellEditor // + //---------------// + @Override + public TableCellEditor getCellEditor (int row, + int col) + { + UnitModel.Column column = UnitModel.Column.values()[col]; + + switch (column) { + case MODIF: { + Object node = nodeForRow(row); + + if (node instanceof Constant) { + return getDefaultEditor(Boolean.class); + } + } + + break; + + case VALUE: { + Object obj = getModel().getValueAt(row, col); + + if (obj instanceof Boolean) { + return getDefaultEditor(Boolean.class); + } + } + + break; + + default: + } + + // Default cell editor (determined by column class) + return super.getCellEditor(row, col); + } + + //-----------------// + // getCellRenderer // + //-----------------// + /** + * Used by the UI to get the proper renderer for each given cell in + * the table. + * + * @param row row in the table + * @param col column in the table + * @return the best renderer for the cell. + */ + @Override + public TableCellRenderer getCellRenderer (int row, + int col) + { + UnitModel.Column column = UnitModel.Column.values()[col]; + + switch (column) { + case MODIF: { + Object obj = getModel().getValueAt(row, col); + + if (obj instanceof Boolean) { + // A constant => Modif flag + return getDefaultRenderer(Boolean.class); + } else { + // A node (unit or package) + return getDefaultRenderer(Object.class); + } + } + + case VALUE: { + Object obj = getModel().getValueAt(row, col); + + if (obj instanceof Boolean) { + return getDefaultRenderer(Boolean.class); + } else { + return valueRenderer; + } + } + + case PIXEL: + return pixelRenderer; + + default: + return getDefaultRenderer(getColumnClass(col)); + } + } + + //--------------------// + // scrollRowToVisible // + //--------------------// + /** + * Scroll so that the provided row gets visible + * + * @param row the provided row + */ + public void scrollRowToVisible (int row) + { + final int height = tree.getRowHeight(); + Rectangle rect = new Rectangle(0, row * height, 0, 0); + + if (getParent() instanceof JComponent) { + JComponent comp = (JComponent) getParent(); + rect.grow(0, comp.getHeight() / 2); + } else { + rect.grow(0, height); + } + + scrollRectToVisible(rect); + } + + //-------------------// + // setNodesSelection // + //-------------------// + /** + * Select the rows that correspond to the provided nodes + * + * @param matches the nodes to select + * @return the relevant rows + */ + public List setNodesSelection (Collection matches) + { + List paths = new ArrayList<>(); + + for (Object object : matches) { + if (object instanceof Constant) { + Constant constant = (Constant) object; + TreePath path = getPath(constant, constant.getQualifiedName()); + paths.add(path); + } else if (object instanceof Node) { + Node node = (Node) object; + TreePath path = getPath(node, node.getName()); + paths.add(path); + } + } + + // Selection on tree side + tree.setSelectionPaths(paths.toArray(new TreePath[paths.size()])); + + // Selection on table side + clearSelection(); + + List rows = new ArrayList<>(); + + for (TreePath path : paths) { + int row = tree.getRowForPath(path); + + if (row != -1) { + rows.add(row); + addRowSelectionInterval(row, row); + } + } + + Collections.sort(rows); + + return rows; + } + + //---------------// + // adjustColumns // + //---------------// + /** + * Allows to adjust the related columnModel, for each and every + * column + * + * @param cModel the proper table column model + */ + private void adjustColumns () + { + TableColumnModel cModel = getColumnModel(); + + // Columns widths + for (UnitModel.Column c : UnitModel.Column.values()) { + cModel.getColumn(c.ordinal()).setPreferredWidth(c.getWidth()); + } + } + + //---------// + // getPath // + //---------// + private TreePath getPath (Object object, + String fullName) + { + UnitManager unitManager = UnitManager.getInstance(); + List objects = new ArrayList<>(); + objects.add(unitManager.getRoot()); + + int dotPos = -1; + + while ((dotPos = fullName.indexOf('.', dotPos + 1)) != -1) { + String path = fullName.substring(0, dotPos); + objects.add(unitManager.getNode(path)); + } + + objects.add(object); + + logger.debug("path to {} objects:{}", fullName, objects); + + return new TreePath(objects.toArray()); + } + + //------------// + // nodeForRow // + //------------// + /** + * Return the tree node facing the provided table row + * + * @param row the provided row + * @return the corresponding tree node + */ + private Object nodeForRow (int row) + { + return ((TreeTableModelAdapter) getModel()).nodeForRow(row); + } + + //-------------------// + // preExpandPackages // + //-------------------// + /** + * Before displaying the tree, expand all nodes that correspond to + * packages (PackageNode). + */ + private void preExpandPackages () + { + for (int row = 0; row < tree.getRowCount(); row++) { + if (tree.isCollapsed(row)) { + tree.expandRow(row); + } + } + } + + //~ Inner Classes ---------------------------------------------------------- + + //---------------// + // PixelRenderer // + //---------------// + private static class PixelRenderer + extends DefaultTableCellRenderer + { + //~ Methods ------------------------------------------------------------ + + @Override + public Component getTableCellRendererComponent (JTable table, + Object value, + boolean isSelected, + boolean hasFocus, + int row, + int column) + { + super.getTableCellRendererComponent( + table, + value, + isSelected, + hasFocus, + row, + column); + + // Use right alignment + setHorizontalAlignment(SwingConstants.RIGHT); + + return this; + } + } + + //---------------// + // ValueRenderer // + //---------------// + private static class ValueRenderer + extends DefaultTableCellRenderer + { + //~ Methods ------------------------------------------------------------ + + @Override + public Component getTableCellRendererComponent (JTable table, + Object value, + boolean isSelected, + boolean hasFocus, + int row, + int column) + { + super.getTableCellRendererComponent( + table, + value, + isSelected, + hasFocus, + row, + column); + + // Use a bold font + setFont(table.getFont().deriveFont(Font.BOLD).deriveFont(12.0f)); + + // Use center alignment + setHorizontalAlignment(SwingConstants.CENTER); + + return this; + } + } +} diff --git a/src/main/omr/constant/doc-files/Constant.uxf b/src/main/omr/constant/doc-files/Constant.uxf new file mode 100644 index 0000000..db74c18 --- /dev/null +++ b/src/main/omr/constant/doc-files/Constant.uxf @@ -0,0 +1,30 @@ +//Uncomment the following line to change the fontsize: +//fontsize=14 + +//Welcome to UMLet! + +// *Double-click on UML elements to add them to the diagram. +// *Edit element properties by modifying the text in this panel. +// *Edit the files in the 'palettes' directory to store your own element palettes. +// *Press Del or Backspace to remove elements from the diagram. +// *Hold down Ctrl key to select multiple elements. +// *Press c to copy the UML diagram to the system clipboard. +// * This text will be stored with each diagram. Feel free to use the area for notes. +com.umlet.element.custom.Database60022012050User Propertiescom.umlet.element.custom.Database60029012050Default Propertiescom.umlet.element.base.Relation45037017040lt=<[<] -20;20;150;20com.umlet.element.custom.Database60036012050Source Codecom.umlet.element.base.Relation5302609080lt=<[<] -20;20;70;60com.umlet.element.base.Relation5302309040lt=<[<] - [>]>20;20;70;20com.umlet.element.base.Relation4202804080lt=<<<-20;20;20;60com.umlet.element.base.Class41023014070ConstantManager +com.umlet.element.base.Class4103010040UnitTreeTablecom.umlet.element.base.Relation2402019040lt=<-20;20;170;20com.umlet.element.base.Class1603010030UnitManagercom.umlet.element.base.Relation34026040100lt=<<<->20;20;20;80com.umlet.element.base.Class330340140190Constant +-- +String quantityUnit +String defaultString +String description +-- +String unit +String name +String qualifiedName +-- +String initialString +String currentString +Object cachedValuecom.umlet.element.base.Class29023010050ConstantSet +-- +String unitcom.umlet.element.base.Class2901707040Logger +-- +levelcom.umlet.element.base.Relation60110140120lt=<<<->20;100;20;20;120;20com.umlet.element.base.Class2021010030PackageNodecom.umlet.element.base.Relation1904040100lt=<<<<->20;20;20;80com.umlet.element.base.Relation100130130110lt=<<-110;20;20;90com.umlet.element.base.Class1702107030UnitNodecom.umlet.element.base.Relation19013040100lt=<<-20;20;20;80com.umlet.element.base.Relation2201809060lt=<-70;20;20;40com.umlet.element.base.Class1801206030/Node/com.umlet.element.base.Relation2202009060lt=<-70;40;20;20 \ No newline at end of file diff --git a/src/main/omr/constant/package.html b/src/main/omr/constant/package.html new file mode 100644 index 0000000..9dec81b --- /dev/null +++ b/src/main/omr/constant/package.html @@ -0,0 +1,82 @@ + + + + + + Package omr.constant + + + +

+ The purpose of this package is to handle application logical + constants in a common way, from their definition in their hosting + classes, their potential on-line modification, and their + persistency on disk. If one day Audiveris migrates to NetBeans + platform, the bulk of this package should disappear. +

+ +

+ Persistency of constants +

+

+ Constant instances represent a logical + application constant, whose persistency is to be managed from one + application run to the other. +

+

+ A constant (and its subclasses) can be: +

+ +
    +
  • A Constant declaration enclosed in a ConstantSet instance. This is by far the + most common and easiest way to define constants related to specific + application class. +
  • +
  • A standalone Constant defined outside the scope of any + ConstantSet. This case is typically used when the name of the + constant must be forged programmatically. +
  • +
+

+ Modification of a Constant value must preferably be + performed through dedicated interfaces. You can directly edit the + property files, but you do so at your own risks. There are two + constant-related GUI within the application: +

+ +
    +
  • The UnitTreeTable is the GUI + related to the UnitManager singleton + which handles all the units (classes) for which there is either a + Logger instance or a ConstantSet instance, or both. This GUI is + launched from Tools | Options menu and handles the tree of these + units. +
  • +
  • The ShapeColorChooser is a + rudimentary GUI interface to set the color of each and every glyph + shape. +
  • + +
+

+ Persistency of each constant is handled by the ConstantManager singleton, which uses + the qualified name of the constant as the key to retrieve a related + property (if any) specified in either the DEFAULT and/or the USER + property files. Please refer to the ConstantManager documentation to read + the details on how a constant value is determined.
+ Since the persistency of a Constant uses its fully qualified name + (i.e. the path to the enclosing class, plus the name of the + constant element in the ConstantSet), the determination of the + fully qualified name is deferred until the value of the Constant is + actually retrieved. This is implemented through the use of a + DirtySet within the UnitManager. +

+ + + diff --git a/src/main/omr/glyph/AbstractEvaluationEngine.java b/src/main/omr/glyph/AbstractEvaluationEngine.java new file mode 100644 index 0000000..cba0896 --- /dev/null +++ b/src/main/omr/glyph/AbstractEvaluationEngine.java @@ -0,0 +1,459 @@ +//----------------------------------------------------------------------------// +// // +// A b s t r a c t G l y p h E v a l u a t o r // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.glyph; + +import omr.WellKnowns; + +import omr.constant.ConstantSet; + +import omr.glyph.ShapeEvaluator.Condition; +import static omr.glyph.ShapeEvaluator.Condition.*; +import omr.glyph.facets.Glyph; + +import omr.sheet.Scale; +import omr.sheet.SystemInfo; + +import omr.util.Predicate; +import omr.util.UriUtil; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.URI; +import java.util.ArrayList; +import java.util.EnumSet; +import java.util.List; + +import javax.swing.JOptionPane; +import javax.xml.bind.JAXBException; + +/** + * Class {@code AbstractEvaluationEngine} is an abstract implementation + * for any evaluation engine. + * + *

+ * + * @author Hervé Bitteur + */ +public abstract class AbstractEvaluationEngine + implements EvaluationEngine +{ + //~ Static fields/initializers --------------------------------------------- + + /** Specific application parameters */ + private static final Constants constants = new Constants(); + + /** Usual logger utility */ + private static final Logger logger = LoggerFactory.getLogger( + AbstractEvaluationEngine.class); + + /** Number of shapes to differentiate. */ + protected static final int shapeCount = 1 + + Shape.LAST_PHYSICAL_SHAPE.ordinal(); + + /** A special evaluation array, used to report NOISE. */ + protected static final Evaluation[] noiseEvaluations = { + new Evaluation(Shape.NOISE, Evaluation.ALGORITHM) + }; + + //~ Instance fields -------------------------------------------------------- + // + /** The glyph checker for additional specific checks. */ + protected ShapeChecker glyphChecker = ShapeChecker.getInstance(); + + //~ Methods ---------------------------------------------------------------- + // + //----------// + // evaluate // + //----------// + @Override + public Evaluation[] evaluate (Glyph glyph, + SystemInfo system, + int count, + double minGrade, + EnumSet conditions, + Predicate predicate) + { + List best = new ArrayList<>(); + Evaluation[] evals = getRawEvaluations(glyph); + + EvalsLoop: + for (Evaluation eval : evals) { + // Bounding test? + if ((best.size() >= count) || (eval.grade < minGrade)) { + break; + } + + // Predicate? + if ((predicate != null) && !predicate.check(eval.shape)) { + continue; + } + + // Allowed? + if (conditions.contains(Condition.ALLOWED) + && glyph.isShapeForbidden(eval.shape)) { + continue; + } + + // Successful checks? + if (conditions.contains(Condition.CHECKED)) { + Evaluation oldEval = new Evaluation(eval.shape, eval.grade); + double[] ins = ShapeDescription.features(glyph); + // This may change the eval shape... + glyphChecker.annotate(system, eval, glyph, ins); + + if (eval.failure != null) { + continue; + } + + // In case the specific checks have changed eval shape + // we have to retest against the glyph blacklist + if ((eval.shape != oldEval.shape) + && conditions.contains(Condition.ALLOWED) + && glyph.isShapeForbidden(eval.shape)) { + continue; + } + } + + // Everything is OK, add the shape if not already in the list + for (Evaluation e : best) { + if (e.shape == eval.shape) { + continue EvalsLoop; + } + } + best.add(eval); + } + + return best.toArray(new Evaluation[0]); + } + + //-------------// + // isBigEnough // + //-------------// + @Override + public boolean isBigEnough (Glyph glyph) + { + return glyph.getNormalizedWeight() >= constants.minWeight.getValue(); + } + + //---------// + // marshal // + //---------// + /** + * Store the engine in XML format, always as a user file. + */ + @Override + public void marshal () + { + final File file = new File(WellKnowns.EVAL_FOLDER, getFileName()); + OutputStream os = null; + + try { + os = new FileOutputStream(file); + marshal(os); + logger.info("Engine marshalled to {}", file); + } catch (FileNotFoundException ex) { + logger.warn("Could not find file " + file, ex); + } catch (IOException ex) { + logger.warn("IO error on file " + file, ex); + } catch (JAXBException ex) { + logger.warn("Error marshalling engine to " + file, ex); + } finally { + if (os != null) { + try { + os.close(); + } catch (Exception ignored) { + } + } + } + } + + //---------// + // rawVote // + //---------// + @Override + public Evaluation rawVote (Glyph glyph, + double minGrade, + Predicate predicate) + { + Evaluation[] evals = evaluate(glyph, null, 1, minGrade, + EnumSet.of(ALLOWED), predicate); + + if (evals.length > 0) { + return evals[0]; + } else { + return null; + } + } + + //------// + // stop // + //------// + /** + * Stop the on-going training. + * By default, this is a no-op + */ + @Override + public void stop () + { + } + + //------// + // Vote // + //------// + @Override + public Evaluation vote (Glyph glyph, + SystemInfo system, + double minGrade, + EnumSet conditions, + Predicate predicate) + { + Evaluation[] evals = evaluate(glyph, system, 1, minGrade, conditions, + predicate); + + if (evals.length > 0) { + return evals[0]; + } else { + return null; + } + } + + //------// + // vote // + //------// + @Override + public Evaluation vote (Glyph glyph, + SystemInfo system, + double minGrade, + Predicate predicate) + { + Evaluation[] evals = evaluate(glyph, system, 1, minGrade, + EnumSet.of(ALLOWED, CHECKED), predicate); + + if (evals.length > 0) { + return evals[0]; + } else { + return null; + } + } + + //------// + // vote // + //------// + @Override + public Evaluation vote (Glyph glyph, + SystemInfo system, + double minGrade) + { + Evaluation[] evals = evaluate(glyph, system, 1, minGrade, + EnumSet.of(ALLOWED, CHECKED), null); + + if (evals.length > 0) { + return evals[0]; + } else { + return null; + } + } + + //-------------// + // getFileName // + //-------------// + /** + * Report the simple file name, including extension but excluding + * parent, which contains the marshalled data of the evaluator. + * + * @return the file name + */ + protected abstract String getFileName (); + + //-------------------// + // getRawEvaluations // + //-------------------// + /** + * Run the evaluator with the specified glyph, and return a + * sequence of interpretations (ordered from best to worst) with + * no additional check. + * + * @param glyph the glyph to be examined + * @return the ordered best evaluations + */ + protected abstract Evaluation[] getRawEvaluations (Glyph glyph); + + //---------// + // marshal // + //---------// + protected abstract void marshal (OutputStream os) + throws FileNotFoundException, IOException, JAXBException; + + //-----------// + // unmarshal // + //-----------// + /** + * The specific unmarshalling method which builds a suitable engine. + * + * @param is the input stream to read + * @return the newly built evaluation engine + * @throws JAXBException, IOException + */ + protected abstract Object unmarshal (InputStream is) + throws JAXBException, IOException; + + //-----------// + // unmarshal // + //-----------// + /** + * Unmarshal the evaluation engine from the most suitable file. + * If a user file does not exist or cannot be unmarshalled, the + * system default file is used + * + * @return the unmarshalled engine, or null if everything failed + */ + protected Object unmarshal () + { + // First try user file, if any (in user EVAL folder) + { + File file = new File(WellKnowns.EVAL_FOLDER, getFileName()); + if (file.exists()) { + Object obj = unmarshal(file); + + if (obj == null) { + logger.warn("Could not load {}", file); + } else { + if (!isCompatible(obj)) { + final String msg = "Obsolete user data for " + getName() + + " in " + file + + ", trying default data"; + logger.warn(msg); + JOptionPane.showMessageDialog(null, msg); + } else { + // Tell the user we are not using the default + logger.info("{} unmarshalled from {}", getName(), file); + return obj; + } + } + } + } + + // Use default file (in program RES folder) + //file = new File(WellKnowns.RES_URI, getFileName()); + URI uri = UriUtil.toURI(WellKnowns.RES_URI, getFileName()); + InputStream input; + try { + input = uri.toURL().openStream(); + } catch (Exception ex) { + logger.warn("Error in " + uri, ex); + return null; + } + Object obj = unmarshal(input, getFileName()); + + if (obj == null) { + logger.warn("Could not load {}", uri); + } else { + if (!isCompatible(obj)) { + final String msg = "Obsolete default data for " + getName() + + " in " + uri + + ", please retrain from scratch"; + logger.warn(msg); + JOptionPane.showMessageDialog(null, msg); + + obj = null; + } else { + logger.debug("{} unmarshalled from {}", getName(), uri); + } + } + + return obj; + } + + //--------------// + // isCompatible // + //--------------// + /** + * Make sure the provided engine object is compatible with the + * current application. + * + * @param obj the engine object + * @return true if engine is usable and found compatible + */ + protected abstract boolean isCompatible (Object obj); + + //-----------// + // unmarshal // + //-----------// + /** + * Unmarshal the evaluation engine using provided file. + * + * @return the unmarshalled engine, or null if failed + */ + private Object unmarshal (File file) + { + try { + InputStream input = new FileInputStream(file); + + return unmarshal(input, getFileName()); + } catch (FileNotFoundException ex) { + logger.warn("File not found " + file, ex); + + return null; + } catch (Exception ex) { + logger.warn("Error unmarshalling from " + file, ex); + + return null; + } + } + + //-----------// + // unmarshal // + //-----------// + private Object unmarshal (InputStream is, + String name) + { + if (is == null) { + logger.warn("No data stream for {} engine as {}", + getName(), name); + } else { + try { + Object engine = unmarshal(is); + is.close(); + + return engine; + } catch (FileNotFoundException ex) { + logger.warn("Cannot find or read " + name, ex); + } catch (IOException ex) { + logger.warn("IO error on " + name, ex); + } catch (JAXBException ex) { + logger.warn("Error unmarshalling evaluator from " + name, ex); + } + } + + return null; + } + + //-----------// + // Constants // + //-----------// + private static final class Constants + extends ConstantSet + { + + Scale.AreaFraction minWeight = new Scale.AreaFraction(0.08, + "Minimum normalized weight to be considered not a noise"); + + } +} diff --git a/src/main/omr/glyph/BasicNest.java b/src/main/omr/glyph/BasicNest.java new file mode 100644 index 0000000..ccf6d8e --- /dev/null +++ b/src/main/omr/glyph/BasicNest.java @@ -0,0 +1,833 @@ +//----------------------------------------------------------------------------// +// // +// B a s i c N e s t // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.glyph; + +import omr.Main; + +import omr.constant.Constant; +import omr.constant.ConstantSet; + +import omr.glyph.facets.Glyph; +import omr.glyph.ui.ViewParameters; + +import omr.lag.BasicRoi; +import omr.lag.Roi; +import omr.lag.Section; +import omr.lag.Sections; + +import omr.math.Histogram; + +import omr.run.Orientation; + +import omr.selection.GlyphEvent; +import omr.selection.GlyphIdEvent; +import omr.selection.GlyphSetEvent; +import omr.selection.LocationEvent; +import omr.selection.MouseMovement; +import omr.selection.NestEvent; +import omr.selection.SelectionHint; +import static omr.selection.SelectionHint.*; +import omr.selection.SelectionService; +import omr.selection.UserEvent; + +import omr.sheet.Sheet; +import omr.sheet.SystemInfo; + +import omr.util.VipUtil; + +import org.bushe.swing.event.EventSubscriber; + +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.HashSet; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * Class {@code BasicNest} implements a {@link Nest}. + * + * @author Hervé Bitteur + */ +public class BasicNest + implements Nest, + EventSubscriber +{ + //~ Static fields/initializers --------------------------------------------- + + /** Specific application parameters */ + private static final Constants constants = new Constants(); + + /** Usual logger utility */ + private static final Logger logger = LoggerFactory.getLogger(BasicNest.class); + + /** Events read on location service */ + public static final Class[] locEventsRead = new Class[]{ + LocationEvent.class}; + + /** Events read on nest (glyph) service */ + public static final Class[] glyEventsRead = new Class[]{ + GlyphIdEvent.class, + GlyphEvent.class, + GlyphSetEvent.class + }; + + //~ Instance fields -------------------------------------------------------- + /** (Debug) a unique name for this nest. */ + private final String name; + + /** Related sheet. */ + private final Sheet sheet; + + /** Elaborated constants for this nest. */ + private final Parameters params; + + /** + * Smart glyph map, based on a physical glyph signature, and thus + * usable across several glyph extractions, to ensure glyph unicity + * whatever the sequential ID it is assigned. + */ + private final ConcurrentHashMap originals = new ConcurrentHashMap<>(); + + /** + * Collection of all glyphs ever inserted in this Nest, indexed by + * glyph id. No non-virtual glyph is ever removed from this map. + */ + private final ConcurrentHashMap allGlyphs = new ConcurrentHashMap<>(); + + /** + * Current map of section -> glyphs. + * This defines the glyphs that are currently active, since there is at + * least one section pointing to them (the sections collection is + * immutable). + * Nota: The glyph reference within the section is kept in sync + */ + private final ConcurrentHashMap activeMap = new ConcurrentHashMap<>(); + + /** + * Collection of active glyphs. + * This is derived from the activeMap, to give direct access to all the + * active glyphs, and is kept in sync with activeMap. + * It also contains the virtual glyphs since these are always active. + */ + private Set activeGlyphs; + + /** Collection of virtual glyphs. (with no underlying sections) */ + private Set virtualGlyphs = new HashSet<>(); + + /** Global id to uniquely identify a glyph. */ + private final AtomicInteger globalGlyphId = new AtomicInteger(0); + + /** Location service (read & write). */ + private SelectionService locationService; + + /** Hosted glyph service. (Glyph, GlyphId and GlyphSet) */ + protected final SelectionService glyphService; + + //~ Constructors ----------------------------------------------------------- + //-----------// + // BasicNest // + //-----------// + /** + * Create a glyph nest. + * + * @param name the distinguished name for this instance + */ + public BasicNest (String name, + Sheet sheet) + { + this.name = name; + this.sheet = sheet; + + params = new Parameters(); + glyphService = new SelectionService(name, Nest.eventsWritten); + } + + //~ Methods ---------------------------------------------------------------- + //----------// + // addGlyph // + //----------// + @Override + public Glyph addGlyph (Glyph glyph) + { + glyph = registerGlyph(glyph); + + // Make absolutely all its sections point back to it + glyph.linkAllSections(); + + if (glyph.isVip()) { + logger.info("{} added", glyph.idString()); + } + + return glyph; + } + + //--------// + // dumpOf // + //--------// + @Override + public String dumpOf (String title) + { + StringBuilder sb = new StringBuilder(); + + if (title != null) { + sb.append(String.format("%s%n", title)); + } + + // Dump of active glyphs + sb.append(String.format("Active glyphs (%s) :%n", + getActiveGlyphs().size())); + + for (Glyph glyph : getActiveGlyphs()) { + sb.append(String.format("%s%n", glyph)); + } + + // Dump of inactive glyphs + Collection inactives = new ArrayList<>(getAllGlyphs()); + inactives.removeAll(getActiveGlyphs()); + sb.append(String.format("%nInactive glyphs (%s) :%n", inactives.size())); + + for (Glyph glyph : inactives) { + sb.append(String.format("%s%n", glyph)); + } + + return sb.toString(); + } + + //-----------------// + // getActiveGlyphs // + //-----------------// + @Override + public synchronized Collection getActiveGlyphs () + { + if (activeGlyphs == null) { + activeGlyphs = Glyphs.sortedSet(activeMap.values()); + activeGlyphs.addAll(virtualGlyphs); + } + + return Collections.unmodifiableCollection(activeGlyphs); + } + + //--------------// + // getAllGlyphs // + //--------------// + @Override + public Collection getAllGlyphs () + { + return Collections.unmodifiableCollection(allGlyphs.values()); + } + + //----------// + // getGlyph // + //----------// + @Override + public Glyph getGlyph (Integer id) + { + return allGlyphs.get(id); + } + + //-----------------// + // getGlyphService // + //-----------------// + @Override + public SelectionService getGlyphService () + { + return glyphService; + } + + //--------------// + // getHistogram // + //--------------// + @Override + public Histogram getHistogram (Orientation orientation, + Collection glyphs) + { + Histogram histo = new Histogram<>(); + + if (!glyphs.isEmpty()) { + Rectangle box = Glyphs.getBounds(glyphs); + Roi roi = new BasicRoi(box); + histo = roi.getSectionHistogram( + orientation, + Glyphs.sectionsOf(glyphs)); + } + + return histo; + } + + //---------// + // getName // + //---------// + @Override + public String getName () + { + return name; + } + + //-------------// + // getOriginal // + //-------------// + @Override + public Glyph getOriginal (Glyph glyph) + { + return getOriginal(glyph.getSignature()); + } + + //-------------// + // getOriginal // + //-------------// + @Override + public Glyph getOriginal (GlyphSignature signature) + { + // Find an old glyph registered with this signature + Glyph oldGlyph = originals.get(signature); + + if (oldGlyph == null) { + return null; + } + + // Check the old signature is still valid + if (oldGlyph.getSignature().compareTo(signature) == 0) { + return oldGlyph; + } else { + logger.debug("Obsolete signature for {}", oldGlyph); + + return null; + } + } + + //------------------// + // getSelectedGlyph // + //------------------// + @Override + public Glyph getSelectedGlyph () + { + return (Glyph) getGlyphService().getSelection(GlyphEvent.class); + } + + //---------------------// + // getSelectedGlyphSet // + //---------------------// + @SuppressWarnings("unchecked") + @Override + public Set getSelectedGlyphSet () + { + return (Set) getGlyphService().getSelection(GlyphSetEvent.class); + } + + //-------// + // isVip // + //-------// + @Override + public boolean isVip (Glyph glyph) + { + return params.vipGlyphs.contains(glyph.getId()); + } + + //--------------// + // lookupGlyphs // + //--------------// + @Override + public Set lookupGlyphs (Rectangle rect) + { + return Glyphs.lookupGlyphs(getActiveGlyphs(), rect); + } + + //-------------------------// + // lookupIntersectedGlyphs // + //-------------------------// + @Override + public Set lookupIntersectedGlyphs (Rectangle rect) + { + return Glyphs.lookupIntersectedGlyphs(getActiveGlyphs(), rect); + } + + //--------------------// + // lookupVirtualGlyph // + //--------------------// + @Override + public Glyph lookupVirtualGlyph (Point point) + { + for (Glyph virtual : virtualGlyphs) { + if (virtual.getBounds().contains(point)) { + return virtual; + } + } + + return null; + } + + //------------// + // mapSection // + //------------// + /** + * Map a section to a glyph, making the glyph active + * + * @param section the section to map + * @param glyph the assigned glyph + */ + @Override + public synchronized void mapSection (Section section, + Glyph glyph) + { + if (glyph != null) { + activeMap.put(section, glyph); + } else { + activeMap.remove(section); + } + + // Invalidate the collection of active glyphs + activeGlyphs = null; + } + + //---------// + // onEvent // + //---------// + @Override + public void onEvent (UserEvent event) + { + try { + // Ignore RELEASING + if (event.movement == MouseMovement.RELEASING) { + return; + } + + if (event instanceof LocationEvent) { + // Location => enclosed Glyph(s) or 1 virtual glyph + handleEvent((LocationEvent) event); + } else if (event instanceof GlyphEvent) { + // Glyph => glyph contour & GlyphSet update + handleEvent((GlyphEvent) event); + } else if (event instanceof GlyphSetEvent) { + // GlyphSet => Compound glyph + handleEvent((GlyphSetEvent) event); + } else if (event instanceof GlyphIdEvent) { + // Glyph Id => Glyph + handleEvent((GlyphIdEvent) event); + } + } catch (Throwable ex) { + logger.warn(getClass().getName() + " onEvent error", ex); + } + } + + //---------------// + // registerGlyph // + //---------------// + @Override + public Glyph registerGlyph (Glyph glyph) + { + // First check this physical glyph does not already exist + Glyph original = getOriginal(glyph); + + if (original != null) { + if (original != glyph) { + // Reuse the existing glyph + if (logger.isDebugEnabled()) { + logger.debug("new avatar of #{}{}{}", + original.getId(), + Sections. + toString(" members", glyph.getMembers()), + Sections.toString(" original", original. + getMembers())); + } + + glyph = original; + glyph.setPartOf(null); + } + } else { + GlyphSignature newSig = glyph.getSignature(); + + if (glyph.isTransient()) { + // Register with a brand new Id + final int id = generateId(); + glyph.setId(id); + glyph.setNest(this); + allGlyphs.put(id, glyph); + + if (isVip(glyph)) { + glyph.setVip(); + } + } else { + // This is a re-registration + GlyphSignature oldSig = glyph.getRegisteredSignature(); + + if ((oldSig != null) && !newSig.equals(oldSig)) { + Glyph oldGlyph = originals.remove(oldSig); + + if (oldGlyph != null) { + logger.debug("Updating registration of {} oldGlyph:{}", + glyph.idString(), oldGlyph.getId()); + } + } + } + + originals.put(newSig, glyph); + glyph.setRegisteredSignature(newSig); + + logger.debug("Registered {} as original {}", + glyph.idString(), glyph.getSignature()); + } + + // Special for virtual glyphs + if (glyph.isVirtual()) { + virtualGlyphs.add(glyph); + } + + return glyph; + } + + //--------------------// + // removeVirtualGlyph // + //--------------------// + @Override + public synchronized void removeVirtualGlyph (VirtualGlyph glyph) + { + originals.remove(glyph.getSignature(), glyph); + allGlyphs.remove(glyph.getId(), glyph); + virtualGlyphs.remove(glyph); + activeGlyphs = null; + } + + //-------------// + // setServices // + //-------------// + @Override + public void setServices (SelectionService locationService) + { + this.locationService = locationService; + + for (Class eventClass : locEventsRead) { + locationService.subscribeStrongly(eventClass, this); + } + + for (Class eventClass : glyEventsRead) { + glyphService.subscribeStrongly(eventClass, this); + } + } + + //-------------// + // setServices // + //-------------// + @Override + public void cutServices (SelectionService locationService) + { + for (Class eventClass : locEventsRead) { + locationService.unsubscribe(eventClass, this); + } + + for (Class eventClass : glyEventsRead) { + glyphService.unsubscribe(eventClass, this); + } + } + + //----------// + // toString // + //----------// + @Override + public String toString () + { + StringBuilder sb = new StringBuilder("{Nest"); + + sb.append(" ").append(name); + + // Active/All glyphs + if (!allGlyphs.isEmpty()) { + sb.append(" glyphs=").append(getActiveGlyphs().size()).append("/"). + append(allGlyphs.size()); + } else { + sb.append(" noglyphs"); + } + + sb.append("}"); + + return sb.toString(); + } + + //---------// + // publish // + //---------// + /** + * Publish on glyph service + * + * @param event the event to publish + */ + protected void publish (NestEvent event) + { + glyphService.publish(event); + } + + //---------// + // publish // + //---------// + /** + * Publish on location service + * + * @param event the event to publish + */ + protected void publish (LocationEvent event) + { + locationService.publish(event); + } + + //------------------// + // subscribersCount // + //------------------// + /** + * Convenient method to retrieve the number of subscribers on the glyph + * service for a specific class + * + * @param classe the specific classe + * @return the number of subscribers interested in the specific class + */ + protected int subscribersCount (Class classe) + { + return glyphService.subscribersCount(classe); + } + + //------------// + // generateId // + //------------// + private int generateId () + { + return globalGlyphId.incrementAndGet(); + } + + //-------------// + // handleEvent // + //-------------// + /** + * Interest in sheet location => [active] glyph(s) + * + * @param locationEvent + */ + private void handleEvent (LocationEvent locationEvent) + { + SelectionHint hint = locationEvent.hint; + MouseMovement movement = locationEvent.movement; + Rectangle rect = locationEvent.getData(); + + if (!hint.isLocation() && !hint.isContext()) { + return; + } + + if (rect == null) { + return; + } + + if ((rect.width > 0) && (rect.height > 0)) { + // This is a non-degenerated rectangle + // Look for set of enclosed active glyphs + Set glyphsFound = lookupGlyphs(rect); + + // Publish Glyph + Glyph glyph = glyphsFound.isEmpty() ? null + : glyphsFound.iterator().next(); + publish(new GlyphEvent(this, hint, movement, glyph)); + + // Publish GlyphSet + publish(new GlyphSetEvent(this, hint, movement, glyphsFound)); + } else { + // This is just a point + Glyph glyph = lookupVirtualGlyph( + new Point(rect.getLocation())); + + // Publish virtual Glyph, if any + if (glyph != null) { + publish(new GlyphEvent(this, hint, movement, glyph)); + } else { + // No virtual glyph found, a standard glyph is found by: + // Pt -> (h/v)run -> (h/v)section -> glyph + // So there is nothing to do here, except nullifying glyph + publish(new GlyphEvent(this, hint, movement, null)); + + // And let proper lag publish non-null glyph later + // Since BasicNest is first subscriber on location (berk!) + } + } + } + + //-------------// + // handleEvent // + //-------------// + /** + * Interest in Glyph => glyph contour & GlyphSet update + * + * @param glyphEvent + */ + private void handleEvent (GlyphEvent glyphEvent) + { + SelectionHint hint = glyphEvent.hint; + MouseMovement movement = glyphEvent.movement; + Glyph glyph = glyphEvent.getData(); + + if ((hint == GLYPH_INIT) || (hint == GLYPH_MODIFIED)) { + // Display glyph contour + if (glyph != null) { + Rectangle box = glyph.getBounds(); + publish(new LocationEvent(this, hint, movement, box)); + } + } + + // In glyph-selection mode, for non-transient glyphs + // (and only if we have interested subscribers) + if ((hint != GLYPH_TRANSIENT) + && !ViewParameters.getInstance().isSectionMode() + && (subscribersCount(GlyphSetEvent.class) > 0)) { + // Update glyph set + Set glyphs = getSelectedGlyphSet(); + + if (glyphs == null) { + glyphs = new LinkedHashSet<>(); + } + + if (hint == LOCATION_ADD) { + // Adding to (or Removing from) the set of glyphs + if (glyph != null) { + if (glyphs.contains(glyph)) { + glyphs.remove(glyph); + } else { + glyphs.add(glyph); + } + } + } else if (hint == CONTEXT_ADD) { + // Don't modify the set + } else { + // Overwriting the set of glyphs + if (glyph != null) { + // Make a one-glyph set + glyphs = Glyphs.sortedSet(glyph); + } else { + // Make an empty set + glyphs = Glyphs.sortedSet(); + } + } + + publish(new GlyphSetEvent(this, hint, movement, glyphs)); + } + } + + //-------------// + // handleEvent // + //-------------// + /** + * Interest in GlyphSet => Compound + * + * @param glyphSetEvent + */ + private void handleEvent (GlyphSetEvent glyphSetEvent) + { + if (ViewParameters.getInstance().isSectionMode()) { + // Section mode + return; + } + + // Glyph mode + MouseMovement movement = glyphSetEvent.movement; + Set glyphs = glyphSetEvent.getData(); + Glyph compound = null; + + if ((glyphs != null) && (glyphs.size() > 1)) { + try { + SystemInfo system = sheet.getSystemOf(glyphs); + + if (system != null) { + compound = system.buildTransientCompound(glyphs); + publish( + new GlyphEvent( + this, + SelectionHint.GLYPH_TRANSIENT, + movement, + compound)); + } + } catch (IllegalArgumentException ex) { + // All glyphs do not belong to the same system + // No compound is allowed and displayed + logger.warn("Selecting glyphs from different systems"); + } + } + } + + //-------------// + // handleEvent // + //-------------// + /** + * Interest in Glyph ID => glyph + * + * @param glyphIdEvent + */ + private void handleEvent (GlyphIdEvent glyphIdEvent) + { + SelectionHint hint = glyphIdEvent.hint; + MouseMovement movement = glyphIdEvent.movement; + int id = glyphIdEvent.getData(); + + //TODO: Check the need for this: + // // Nullify Run entity + // publish(new RunEvent(this, hint, movement, null)); + // + // // Nullify Section entity + // publish(new SectionEvent(this, hint, movement, null)); + + // Report Glyph entity (which may be null) + publish(new GlyphEvent(this, hint, movement, getGlyph(id))); + } + + //~ Inner Classes ---------------------------------------------------------- + //-----------// + // Constants // + //-----------// + private static final class Constants + extends ConstantSet + { + //~ Instance fields ---------------------------------------------------- + + Constant.String vipGlyphs = new Constant.String( + "", + "(Debug) Comma-separated list of VIP glyphs"); + + } + + //------------// + // Parameters // + //------------// + /** + * Class {@code Parameters} gathers all constants related to nest + */ + private static class Parameters + { + //~ Instance fields ---------------------------------------------------- + + final List vipGlyphs; // List of IDs for VIP glyphs + + //~ Constructors ------------------------------------------------------- + public Parameters () + { + vipGlyphs = VipUtil.decodeIds(constants.vipGlyphs.getValue()); + + if (logger.isDebugEnabled()) { + Main.dumping.dump(this); + } + + if (!vipGlyphs.isEmpty()) { + logger.info("VIP glyphs: {}", vipGlyphs); + } + } + } +} diff --git a/src/main/omr/glyph/CompoundBuilder.java b/src/main/omr/glyph/CompoundBuilder.java new file mode 100644 index 0000000..780c4c7 --- /dev/null +++ b/src/main/omr/glyph/CompoundBuilder.java @@ -0,0 +1,462 @@ +//----------------------------------------------------------------------------// +// // +// C o m p o u n d B u i l d e r // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.glyph; + +import omr.glyph.facets.Glyph; + +import omr.sheet.SystemInfo; + +import omr.util.Predicate; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.awt.Rectangle; +import java.util.ArrayList; +import java.util.Collection; +import java.util.EnumSet; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** + * Class {@code CompoundBuilder} defines a generic way to smartly + * build glyph compounds, and provides derived variants. + * + * @author Hervé Bitteur + */ +public class CompoundBuilder +{ + //~ Static fields/initializers --------------------------------------------- + + /** Usual logger utility */ + private static final Logger logger = LoggerFactory.getLogger( + CompoundBuilder.class); + + //~ Instance fields -------------------------------------------------------- + /** Dedicated system */ + protected final SystemInfo system; + + //~ Constructors ----------------------------------------------------------- + //-----------------// + // CompoundBuilder // + //-----------------// + /** + * Creates a new CompoundBuilder object. + * + * @param system the containing system + */ + public CompoundBuilder (SystemInfo system) + { + this.system = system; + } + + //~ Methods ---------------------------------------------------------------- + //---------------// + // buildCompound // + //---------------// + /** + * Try to build a compound, starting from given seed and looking + * into the collection of suitable glyphs. + * + *

If successful, this method assigns the proper shape to the compound, + * and inserts it in the system environment. + * + * @param seed the initial glyph around which the compound is built + * @param includeSeed true if seed must be included in compound + * @param suitables collection of potential glyphs + * @param adapter the specific behavior of the compound tests + * @return the compound built if successful, null otherwise + */ + public Glyph buildCompound (Glyph seed, + boolean includeSeed, + Collection suitables, + CompoundAdapter adapter) + { + // Set seed (and reference box) + adapter.setSeed(seed); + + // Retrieve good neighbors among the suitable glyphs + Set neighbors = new HashSet<>(); + + // Include the seed in the compound glyphs? + int minCount = 1; + + if (includeSeed) { + neighbors.add(seed); + minCount++; + } + + for (Glyph g : suitables) { + if (includeSeed || (g != seed)) { + if (adapter.isCandidateSuitable(g) + && adapter.isCandidateClose(g)) { + neighbors.add(g); + } + } + } + + if (neighbors.size() >= minCount) { + if (logger.isDebugEnabled()) { + logger.debug("neighbors={} seed={}", + Glyphs.toString(neighbors), seed); + } + + Glyph compound = system.buildTransientCompound(neighbors); + + if (adapter.isCompoundValid(compound)) { + // Assign and insert into system & nest environments + compound = system.addGlyph(compound); + compound.setEvaluation(adapter.getChosenEvaluation()); + + logger.debug("Compound #{} built as {}", + compound.getId(), compound.getShape()); + + return compound; + } + } + + return null; + } + + //---------------// + // buildCompound // + //---------------// + /** + * A basic building, which simply takes all the provided glyphs + * and build a persistent compound out of them. + * + * @param parts the glyphs to merge + * @return the compound built + */ + public Glyph buildCompound (Collection parts) + { + if (parts.isEmpty()) { + return null; + } + + List list = new ArrayList<>(parts); + + return buildCompound( + list.get(0), + true, + list.subList(1, list.size()), + new NoAdapter(system)); + } + + //~ Inner Interfaces ------------------------------------------------------- + //-----------------// + // CompoundAdapter // + //-----------------// + /** + * Interface {@code CompoundAdapter} provides the needed features + * for building compounds out of glyphs. + */ + public static interface CompoundAdapter + { + //~ Methods ------------------------------------------------------------ + + /** + * Report the evaluation chosen for the compound. + * + * @return the evaluation (shape + grade) chosen + */ + Evaluation getChosenEvaluation (); + + /** + * Predicate to check whether a given candidate glyph is close + * enough to the reference box. + * + * @param glyph the glyph to check for proximity + * @return true if glyph is close enough + */ + boolean isCandidateClose (Glyph glyph); + + /** + * Predicate for a glyph to be a potential part of the building. + * (the location criteria is handled by {@link #isCandidateClose}). + * + * @param glyph the glyph to check + * @return true if the glyph is suitable for inclusion + */ + boolean isCandidateSuitable (Glyph glyph); + + /** + * Predicate to check the validity of the newly built compound. + * If valid, the chosenEvaluation is assigned accordingly. + * + * @param compound the resulting compound glyph to check + * @return true if the compound is found OK. The compound shape is not + * assigned by this method, but can be later retrieved through + * getChosenEvaluation() method. + */ + boolean isCompoundValid (Glyph compound); + + /** + * Define the seed glyph around which the compound will be built. + * + * @param seed the seed glyph + * @return the computed reference box + */ + Rectangle setSeed (Glyph seed); + + /** + * Should we filter the provided candidates?. (by calling + * {@link #isCandidateSuitable} and {@link #isCandidateClose}). + * + * @return true to apply filter + */ + boolean shouldFilterCandidates (); + } + + //~ Inner Classes ---------------------------------------------------------- + //-----------------// + // AbstractAdapter // + //-----------------// + /** + * Basic abstract class to implement the {@link CompoundAdapter} + * interface. + */ + public abstract static class AbstractAdapter + implements CompoundAdapter + { + //~ Instance fields ---------------------------------------------------- + + /** Dedicated system */ + protected final SystemInfo system; + + /** Maximum grade for a compound */ + protected final double minGrade; + + /** Originating seed */ + protected Glyph seed; + + /** Search box */ + protected Rectangle box; + + /** The result of compound evaluation */ + protected Evaluation chosenEvaluation; + + //~ Constructors ------------------------------------------------------- + /** + * Construct an AbstractAdapter. + * + * @param system the containing system + * @param minGrade maximum acceptable grade for the compound shape + */ + public AbstractAdapter (SystemInfo system, + double minGrade) + { + this.system = system; + this.minGrade = minGrade; + } + + //~ Methods ------------------------------------------------------------ + @Override + public Evaluation getChosenEvaluation () + { + // By default, use shape and grade from evaluator + return chosenEvaluation; + } + + @Override + public boolean isCandidateClose (Glyph glyph) + { + // By default, use box intersection + return box.intersects(glyph.getBounds()); + } + + @Override + public Rectangle setSeed (Glyph seed) + { + this.seed = seed; + box = computeReferenceBox(); + + return box; + } + + @Override + public boolean shouldFilterCandidates () + { + // By default, filter candidates + return true; + } + + /** + * Compute the reference box. + * This method is called when seed has just been set. + */ + protected abstract Rectangle computeReferenceBox (); + } + + //-----------// + // NoAdapter // + //-----------// + /** + * A passthrough fake adapter. + */ + public static class NoAdapter + extends AbstractAdapter + { + //~ Constructors ------------------------------------------------------- + + public NoAdapter (SystemInfo system) + { + super(system, 0); + } + + //~ Methods ------------------------------------------------------------ + @Override + public Evaluation getChosenEvaluation () + { + return new Evaluation(null, Evaluation.ALGORITHM); + } + + @Override + public boolean isCandidateClose (Glyph glyph) + { + return true; + } + + @Override + public boolean isCandidateSuitable (Glyph glyph) + { + return true; + } + + @Override + public boolean isCompoundValid (Glyph compound) + { + return true; + } + + @Override + public boolean shouldFilterCandidates () + { + return false; + } + + @Override + protected Rectangle computeReferenceBox () + { + return null; + } + } + + //---------------// + // TopRawAdapter // + //---------------// + /** + * This compound adapter tries to find some specific shapes among + * the top raw evaluations found for the compound. + */ + public abstract static class TopRawAdapter + extends AbstractAdapter + { + //~ Instance fields ---------------------------------------------------- + + /** Collection of desired shapes for a valid compound */ + protected final EnumSet desiredShapes; + + /** Specific predicate for desired shapes */ + protected final Predicate predicate = new Predicate() + { + @Override + public boolean check (Shape shape) + { + return desiredShapes.contains(shape); + } + }; + + //~ Constructors ------------------------------------------------------- + /** + * Create a TopRawAdapter instance. + * + * @param system the containing system + * @param minGrade maximum acceptable grade on compound shape + * @param desiredShapes the valid shapes for the compound + */ + public TopRawAdapter (SystemInfo system, + double minGrade, + EnumSet desiredShapes) + { + super(system, minGrade); + this.desiredShapes = desiredShapes; + } + + //~ Methods ------------------------------------------------------------ + @Override + public boolean isCompoundValid (Glyph compound) + { + // Check if a desired shape appears in the top raw evaluations + final Evaluation vote = GlyphNetwork.getInstance().rawVote( + compound, + minGrade, + predicate); + + if (vote != null) { + chosenEvaluation = vote; + + return true; + } else { + return false; + } + } + } + + //-----------------// + // TopShapeAdapter // + //-----------------// + /** + * This compound adapter tries to find some specific shapes among + * the top evaluations found for the compound. + */ + public abstract static class TopShapeAdapter + extends TopRawAdapter + { + //~ Constructors ------------------------------------------------------- + + /** + * Create a TopShapeAdapter instance. + * + * @param system the containing system + * @param minGrade maximum acceptable grade on compound shape + * @param desiredShapes the valid shapes for the compound + */ + public TopShapeAdapter (SystemInfo system, + double minGrade, + EnumSet desiredShapes) + { + super(system, minGrade, desiredShapes); + } + + //~ Methods ------------------------------------------------------------ + @Override + public boolean isCompoundValid (Glyph compound) + { + // Check if a desired shape appears in the top evaluations + final Evaluation vote = GlyphNetwork.getInstance().vote( + compound, + system, + minGrade, + predicate); + + if (vote != null) { + chosenEvaluation = vote; + + return true; + } else { + return false; + } + } + } +} diff --git a/src/main/omr/glyph/Evaluation.java b/src/main/omr/glyph/Evaluation.java new file mode 100644 index 0000000..0d9d77b --- /dev/null +++ b/src/main/omr/glyph/Evaluation.java @@ -0,0 +1,167 @@ +//----------------------------------------------------------------------------// +// // +// E v a l u a t i o n // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.glyph; + +import omr.constant.Constant; + +/** + * Class {@code Evaluation} gathers a glyph shape, its grade and, + * if any, details about its failure (name of the check that failed). + * + * @author Hervé Bitteur + */ +public class Evaluation + implements Comparable +{ + //~ Static fields/initializers --------------------------------------------- + + /** Absolute confidence in shape manually assigned by the user. */ + public static final double MANUAL = 300; + + /** Confidence for in structurally assigned. */ + public static final double ALGORITHM = 200; + + //~ Instance fields -------------------------------------------------------- + /** The evaluated shape. */ + public Shape shape; + + /** + * The evaluation grade (larger is better), generally provided by + * the neural network evaluator in the range 0 - 100. + */ + public double grade; + + /** The specific check that failed, if any. */ + public Failure failure; + + //~ Constructors ----------------------------------------------------------- + //------------// + // Evaluation // + //------------// + /** + * Create an initialized evaluation instance. + * + * @param shape the shape this evaluation measures + * @param grade the measurement result (larger is better) + */ + public Evaluation (Shape shape, + double grade) + { + this.shape = shape; + this.grade = grade; + } + + //~ Methods ---------------------------------------------------------------- + //-----------// + // compareTo // + //-----------// + /** + * To sort from best to worst. + * + * @param that the other evaluation instance + * @return -1,0 or +1 + */ + @Override + public int compareTo (Evaluation that) + { + if (this.grade > that.grade) { + return -1; + } + + if (this.grade < that.grade) { + return +1; + } + + return 0; + } + + //----------// + // toString // + //----------// + @Override + public String toString () + { + StringBuilder sb = new StringBuilder(); + sb.append(shape); + sb.append("("); + + if (grade == MANUAL) { + sb.append("MANUAL"); + } else if (grade == ALGORITHM) { + sb.append("ALGORITHM"); + } else { + sb.append((float) grade); + } + + if (failure != null) { + sb.append(" failure:") + .append(failure); + } + + sb.append(")"); + + return sb.toString(); + } + + //~ Inner Classes ---------------------------------------------------------- + //---------// + // Failure // + //---------// + /** + * A class to handle which specific check has failed in the + * evaluation. + */ + public static class Failure + { + //~ Instance fields ---------------------------------------------------- + + /** The name of the test that failed. */ + public final String test; + + //~ Constructors ------------------------------------------------------- + public Failure (String test) + { + this.test = test; + } + + //~ Methods ------------------------------------------------------------ + @Override + public String toString () + { + return test; + } + } + + //-------// + // Grade // + //-------// + /** + * A subclass of Constant.Double, meant to store a grade constant. + */ + public static class Grade + extends Constant.Double + { + //~ Constructors ------------------------------------------------------- + + /** + * Specific constructor, where unit & name are assigned later. + * + * @param defaultValue the (double) default value + * @param description the semantic of the constant + */ + public Grade (double defaultValue, + java.lang.String description) + { + super("Grade", defaultValue, description); + } + } +} diff --git a/src/main/omr/glyph/EvaluationEngine.java b/src/main/omr/glyph/EvaluationEngine.java new file mode 100644 index 0000000..902ed2b --- /dev/null +++ b/src/main/omr/glyph/EvaluationEngine.java @@ -0,0 +1,88 @@ +//----------------------------------------------------------------------------// +// // +// E v a l u a t i o n E n g i n e // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.glyph; + +import omr.glyph.facets.Glyph; + +import omr.math.NeuralNetwork; + +import java.util.Collection; + +/** + * Interface {@code EvaluationEngine} describes the life-cycle of an + * evaluation engine. + * + * @author Hervé Bitteur + */ +public interface EvaluationEngine + extends ShapeEvaluator +{ + //~ Enumerations ----------------------------------------------------------- + + /** The various modes for starting the training of an evaluator. */ + public static enum StartingMode + { + //~ Enumeration constant initializers ---------------------------------- + + /** Start with the current values. */ + INCREMENTAL, + /** Start from + * scratch, with new initial values. */ + SCRATCH; + + } + + //~ Methods ---------------------------------------------------------------- + /** + * Dump the internals of the engine. + */ + void dump (); + + /** + * Store the engine in XML format. + */ + void marshal (); + + /** + * Stop the on-going training. + */ + void stop (); + + /** + * Train the evaluator on the provided base of sample glyphs. + * + * @param base the collection of glyphs to train the evaluator + * @param monitor a monitoring interface + * @param mode specify the starting mode of the training session + */ + void train (Collection base, + Monitor monitor, + StartingMode mode); + + //~ Inner Interfaces ------------------------------------------------------- + /** + * General monitoring interface to pass information about the + * training of an evaluator when processing a sample glyph. + */ + public static interface Monitor + extends NeuralNetwork.Monitor + { + //~ Methods ------------------------------------------------------------ + + /** + * Entry called when a glyph is being processed. + * + * @param glyph the sample glyph being processed + */ + void glyphProcessed (Glyph glyph); + } +} diff --git a/src/main/omr/glyph/GlyphInspector.java b/src/main/omr/glyph/GlyphInspector.java new file mode 100644 index 0000000..ecb984d --- /dev/null +++ b/src/main/omr/glyph/GlyphInspector.java @@ -0,0 +1,285 @@ +//----------------------------------------------------------------------------// +// // +// G l y p h I n s p e c t o r // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.glyph; + +import omr.constant.ConstantSet; + +import omr.glyph.facets.Glyph; + +import omr.sheet.Scale; +import omr.sheet.SystemInfo; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.awt.Rectangle; +import java.util.ArrayList; +import java.util.EnumSet; +import java.util.List; + +/** + * Class {@code GlyphInspector} is at a system level, dedicated to the + * inspection of retrieved glyphs, their recognition being usually + * based on features used by a shape evaluator. + * + * @author Hervé Bitteur + */ +public class GlyphInspector +{ + //~ Static fields/initializers --------------------------------------------- + + /** Specific application parameters */ + private static final Constants constants = new Constants(); + + /** Usual logger utility */ + private static final Logger logger = LoggerFactory.getLogger(GlyphInspector.class); + + /** Shapes acceptable for a part candidate */ + private static final EnumSet partShapes = EnumSet.of( + Shape.DOT_set, + Shape.NOISE, + Shape.CLUTTER, + Shape.STACCATISSIMO, + Shape.NOTEHEAD_VOID, + Shape.FLAG_1, + Shape.FLAG_1_UP); + + //~ Instance fields -------------------------------------------------------- + /** Dedicated system */ + private final SystemInfo system; + + //~ Constructors ----------------------------------------------------------- + //----------------// + // GlyphInspector // + //----------------// + /** + * Create an GlyphInspector instance. + * + * @param system the dedicated system + */ + public GlyphInspector (SystemInfo system) + { + this.system = system; + } + + //~ Methods ---------------------------------------------------------------- + //----------------// + // evaluateGlyphs // + //----------------// + /** + * All unassigned symbol glyphs of a given system, for which we can + * get a positive vote from the evaluator, are assigned the voted + * shape. + * + * @param minGrade the lower limit on grade to accept an evaluation + */ + public void evaluateGlyphs (double minGrade) + { + ShapeEvaluator evaluator = GlyphNetwork.getInstance(); + + for (Glyph glyph : system.getGlyphs()) { + if (glyph.getShape() == null) { + // Get vote + Evaluation vote = evaluator.vote(glyph, system, minGrade); + + if (vote != null) { + glyph.setEvaluation(vote); + } + } + } + } + + //---------------// + // inspectGlyphs // + //---------------// + /** + * Process the given system, by retrieving unassigned glyphs, + * evaluating and assigning them if OK, or trying compounds + * otherwise. + * + * @param minGrade the minimum acceptable grade for this processing + * @param wide flag for extra wide box + */ + public void inspectGlyphs (double minGrade, + boolean wide) + { + logger.debug("S#{} inspectGlyphs start", system.getId()); + + // For Symbols & Leaves + system.retrieveGlyphs(); + system.removeInactiveGlyphs(); + evaluateGlyphs(minGrade); + system.removeInactiveGlyphs(); + + // For Compounds + retrieveCompounds(minGrade, wide); + system.removeInactiveGlyphs(); + evaluateGlyphs(minGrade); + system.removeInactiveGlyphs(); + } + + //-------------------// + // retrieveCompounds // + //-------------------// + /** + * In the specified system, look for glyphs portions that should be + * considered as parts of compound glyphs. + * + * @param minGrade minimum acceptable grade + * @param wide flag for extra wide box + */ + private void retrieveCompounds (double minGrade, + boolean wide) + { + // Use a copy to avoid concurrent modifications + List glyphs = new ArrayList<>(system.getGlyphs()); + + for (Glyph seed : glyphs) { + // Now process this seed, by looking at neighbors + BasicAdapter adapter = new BasicAdapter(system, minGrade, seed, wide); + + if (adapter.isCandidateSuitable(seed)) { + system.buildCompound(seed, true, glyphs, adapter); + } + } + } + + //~ Inner Classes ---------------------------------------------------------- + //-----------// + // Constants // + //-----------// + private static final class Constants + extends ConstantSet + { + //~ Instance fields ---------------------------------------------------- + + Scale.Fraction boxMargin = new Scale.Fraction( + 0.25, + "Box margin to check intersection with compound"); + + Scale.Fraction boxWiden = new Scale.Fraction( + 0.5, + "Box special abscissa margin to check intersection with compound"); + + } + + //--------------// + // BasicAdapter // + //--------------// + /** + * Class {@code BasicAdapter} is a CompoundAdapter meant to + * retrieve all compounds (in a system). + */ + private static class BasicAdapter + extends CompoundBuilder.AbstractAdapter + { + //~ Instance fields ---------------------------------------------------- + + private Glyph stem = null; + + private int stemX; + + private int stemToSeed; + + private final boolean wide; + + //~ Constructors ------------------------------------------------------- + /** + * Construct a BasicAdapter around a given seed + * + * @param system the containing system + * @param minGrade minimum acceptable grade + */ + public BasicAdapter (SystemInfo system, + double minGrade, + Glyph seed, + boolean wide) + { + super(system, minGrade); + this.wide = wide; + + stem = seed.getFirstStem(); + + if (stem != null) { + // Remember this stem as a border + stemX = stem.getCentroid().x; + stemToSeed = seed.getCentroid().x - stemX; + } + } + + //~ Methods ------------------------------------------------------------ + @Override + public Rectangle computeReferenceBox () + { + if (seed == null) { + throw new NullPointerException( + "Compound seed has not been set"); + } + + Rectangle newBox = seed.getBounds(); + + Scale scale = system.getScoreSystem().getScale(); + int boxMargin = scale.toPixels(GlyphInspector.constants.boxMargin); + int boxWiden = scale.toPixels(GlyphInspector.constants.boxWiden); + + if (wide) { + newBox.grow(boxWiden, boxMargin); + } else { + newBox.grow(boxMargin, boxMargin); + } + + return newBox; + } + + @Override + public boolean isCandidateSuitable (Glyph glyph) + { + Shape shape = glyph.getShape(); + + if (!glyph.isActive() || (shape == Shape.LEDGER)) { + return false; + } + + if (glyph.isKnown() + && (glyph.isManualShape() + || (!partShapes.contains(shape) + && (glyph.getGrade() > Grades.compoundPartMaxGrade)))) { + return false; + } + + // Stay on same side of the stem if any + if ((stem != null)) { + return ((glyph.getCentroid().x - stemX) * stemToSeed) > 0; + } else { + return true; + } + } + + @Override + public boolean isCompoundValid (Glyph compound) + { + Evaluation eval = GlyphNetwork.getInstance().vote(compound, system, + minGrade); + + if ((eval != null) + && eval.shape.isWellKnown() + && (eval.shape != Shape.CLUTTER) + && (!seed.isKnown() || (eval.grade > seed.getGrade()))) { + chosenEvaluation = eval; + + return true; + } else { + return false; + } + } + } +} diff --git a/src/main/omr/glyph/GlyphNetwork.java b/src/main/omr/glyph/GlyphNetwork.java new file mode 100644 index 0000000..08d24ed --- /dev/null +++ b/src/main/omr/glyph/GlyphNetwork.java @@ -0,0 +1,551 @@ +//----------------------------------------------------------------------------// +// // +// G l y p h N e t w o r k // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.glyph; + +import omr.constant.Constant; +import omr.constant.ConstantSet; + +import omr.glyph.facets.Glyph; + +import omr.math.NeuralNetwork; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.EnumMap; +import java.util.List; + +import javax.xml.bind.JAXBException; + +/** + * Class {@code GlyphNetwork} encapsulates a neural network customized + * for glyph recognition. + * It wraps the generic {@link NeuralNetwork} with application + * information, for training, storing, loading and using the neural network. + * + *

The application neural network data is loaded as follows:

    + *
  1. It first tries to find a file named 'eval/neural-network.xml' in the + * application user area. + * If any, this file contains a custom definition of the network, typically + * after a user training.
  2. + * + *
  3. If not found, it falls back reading the default definition from the + * application resource, reading the 'res/neural-network.xml' file in the + * application program area.

+ * + *

After a user training of the neural network, the data is stored as + * the custom definition in the user local file 'eval/neural-network.xml', + * which will be picked up first when the application is run again.

+ * + * @author Hervé Bitteur + */ +public class GlyphNetwork + extends AbstractEvaluationEngine +{ + //~ Static fields/initializers --------------------------------------------- + + /** Specific application parameters */ + private static final Constants constants = new Constants(); + + /** Usual logger utility */ + private static final Logger logger = LoggerFactory.getLogger(GlyphNetwork.class); + + /** The singleton. */ + private static volatile GlyphNetwork INSTANCE; + + /** Neural network file name. */ + private static final String FILE_NAME = "neural-network.xml"; + + //~ Instance fields -------------------------------------------------------- + // + /** The underlying neural network. */ + private NeuralNetwork engine; + + //~ Constructors ----------------------------------------------------------- + // + //--------------// + // GlyphNetwork // + //--------------// + /** + * Private constructor, to create a glyph neural network. + */ + private GlyphNetwork () + { + // Unmarshal from user or default data, if compatible + engine = (NeuralNetwork) unmarshal(); + + if (engine == null) { + // Get a brand new one (not trained) + logger.info("Creating a brand new {}", getName()); + engine = createNetwork(); + } + } + + //~ Methods ---------------------------------------------------------------- + // + //-------------// + // getInstance // + //-------------// + /** + * Report the single instance of GlyphNetwork in the application. + * + * @return the instance + */ + public static GlyphNetwork getInstance () + { + if (INSTANCE == null) { + synchronized (GlyphNetwork.class) { + if (INSTANCE == null) { + INSTANCE = new GlyphNetwork(); + } + } + } + + return INSTANCE; + } + + //--------------// + // isCompatible // + //--------------// + @Override + protected final boolean isCompatible (Object obj) + { + if (obj instanceof NeuralNetwork) { + NeuralNetwork anEngine = (NeuralNetwork) obj; + + if (!Arrays.equals(anEngine.getInputLabels(), + ShapeDescription.getParameterLabels())) { + if (logger.isDebugEnabled()) { + logger.debug("Engine inputs: {}", + Arrays.toString(anEngine.getInputLabels())); + logger.debug("Shape inputs: {}", + Arrays.toString(ShapeDescription.getParameterLabels())); + } + return false; + } + if (!Arrays.equals(anEngine.getOutputLabels(), + ShapeSet.getPhysicalShapeNames())) { + if (logger.isDebugEnabled()) { + logger.debug("Engine outputs: {}", + Arrays.toString(anEngine.getOutputLabels())); + logger.debug("Physical shapes: {}", + Arrays.toString(ShapeSet.getPhysicalShapeNames())); + } + return false; + } + return true; + } else { + return false; + } + } + + //------// + // dump // + //------// + /** + * Dump the internals of the neural network to the standard output. + */ + @Override + public void dump () + { + engine.dump(); + } + + //--------------// + // getAmplitude // + //--------------// + /** + * Selector for the amplitude value (used in initial random values). + * + * @return the amplitude value + */ + public double getAmplitude () + { + return constants.amplitude.getValue(); + } + + //-----------------// + // getLearningRate // + //-----------------// + /** + * Selector of the current value for network learning rate. + * + * @return the current learning rate + */ + public double getLearningRate () + { + return constants.learningRate.getValue(); + } + + //---------------// + // getListEpochs // + //---------------// + /** + * Selector on the maximum numner of training iterations. + * + * @return the upper limit on iteration counter + */ + public int getListEpochs () + { + return constants.listEpochs.getValue(); + } + + //-------------// + // getMaxError // + //-------------// + /** + * Report the error threshold to potentially stop the training + * process. + * + * @return the threshold currently in use + */ + public double getMaxError () + { + return constants.maxError.getValue(); + } + + //-------------// + // getMomentum // + //-------------// + /** + * Report the momentum training value currently in use. + * + * @return the momentum in use + */ + public double getMomentum () + { + return constants.momentum.getValue(); + } + + //---------// + // getName // + //---------// + /** + * Report a name for this network. + * + * @return a simple name + */ + @Override + public final String getName () + { + return "Neural Network"; + } + + //------------// + // getNetwork // + //------------// + /** + * Selector to the encapsulated Neural Network. + * + * @return the neural network + */ + public NeuralNetwork getNetwork () + { + return engine; + } + + //--------------// + // setAmplitude // + //--------------// + /** + * Set the amplitude value for initial random values (UNUSED). + * + * @param amplitude + */ + public void setAmplitude (double amplitude) + { + constants.amplitude.setValue(amplitude); + } + + //-----------------// + // setLearningRate // + //-----------------// + /** + * Dynamically modify the learning rate of the neural network for + * its training task. + * + * @param learningRate new learning rate to use + */ + public void setLearningRate (double learningRate) + { + constants.learningRate.setValue(learningRate); + engine.setLearningRate(learningRate); + } + + //---------------// + // setListEpochs // + //---------------// + /** + * Modify the upper limit on the number of epochs (training + * iterations) for the training process. + * + * @param listEpochs new value for iteration limit + */ + public void setListEpochs (int listEpochs) + { + constants.listEpochs.setValue(listEpochs); + engine.setEpochs(listEpochs); + } + + //-------------// + // setMaxError // + //-------------// + /** + * Modify the error threshold to potentially stop the training + * process. + * + * @param maxError the new threshold value to use + */ + public void setMaxError (double maxError) + { + constants.maxError.setValue(maxError); + engine.setMaxError(maxError); + } + + //-------------// + // setMomentum // + //-------------// + /** + * Modify the value for momentum used from learning epoch to the + * other. + * + * @param momentum the new momentum value to be used + */ + public void setMomentum (double momentum) + { + constants.momentum.setValue(momentum); + engine.setMomentum(momentum); + } + + //------// + // stop // + //------// + /** + * Forward the "Stop" order to the network being trained. + */ + @Override + public void stop () + { + engine.stop(); + } + + //-------// + // train // + //-------// + /** + * Train the network using the provided collection of glyphs. + * + * @param glyphs the provided collection of glyphs + * @param monitor the monitoring entity if any + * @param mode the starting mode of the trainer (scratch or incremental) + */ + @SuppressWarnings("unchecked") + @Override + public void train (Collection glyphs, + Monitor monitor, + StartingMode mode) + { + if (glyphs.isEmpty()) { + logger.warn("No glyph to retrain Neural Network evaluator"); + + return; + } + + int quorum = constants.quorum.getValue(); + + // Determine cardinality for each shape + EnumMap> shapeGlyphs = new EnumMap<>(Shape.class); + + for (Glyph glyph : glyphs) { + Shape shape = glyph.getShape(); + List list = shapeGlyphs.get(shape); + if (list == null) { + list = new ArrayList<>(); + shapeGlyphs.put(shape, list); + } + list.add(glyph); + } + + List newGlyphs = new ArrayList<>(); + + for (List list : shapeGlyphs.values()) { + int card = 0; + boolean first = true; + + if (!list.isEmpty()) { + while (card < quorum) { + for (int i = 0; i < list.size(); i++) { + newGlyphs.add(list.get(i)); + card++; + + if (!first && (card >= quorum)) { + break; + } + } + + first = false; + } + } + } + + // Shuffle the final collection of glyphs + Collections.shuffle(newGlyphs); + + // Build the collection of patterns from the glyph data + double[][] inputs = new double[newGlyphs.size()][]; + double[][] desiredOutputs = new double[newGlyphs.size()][]; + + int ig = 0; + + for (Glyph glyph : newGlyphs) { + double[] ins = ShapeDescription.features(glyph); + inputs[ig] = ins; + + double[] des = new double[shapeCount]; + Arrays.fill(des, 0); + + des[glyph.getShape().getPhysicalShape().ordinal()] = 1; + desiredOutputs[ig] = des; + + ig++; + } + + // Starting options + if (mode == StartingMode.SCRATCH) { + engine = createNetwork(); + } + + // Train on the patterns + engine.train(inputs, desiredOutputs, monitor); + } + + //-------------// + // getFileName // + //-------------// + @Override + protected String getFileName () + { + return FILE_NAME; + } + + //-------------------// + // getRawEvaluations // + //-------------------// + @Override + protected Evaluation[] getRawEvaluations (Glyph glyph) + { + // If too small, it's just NOISE + if (!isBigEnough(glyph)) { + return noiseEvaluations; + } else { + double[] ins = ShapeDescription.features(glyph); + double[] outs = new double[shapeCount]; + Evaluation[] evals = new Evaluation[shapeCount]; + Shape[] values = Shape.values(); + + engine.run(ins, null, outs); + + for (int s = 0; s < shapeCount; s++) { + Shape shape = values[s]; + // Use a grade in 0 .. 100 range + evals[s] = new Evaluation(shape, 100 * outs[s]); + } + + // Order the evals from best to worst + Arrays.sort(evals); + + return evals; + } + } + + //---------// + // marshal // + //---------// + @Override + protected void marshal (OutputStream os) + throws FileNotFoundException, IOException, JAXBException + { + engine.marshal(os); + } + + //-----------// + // unmarshal // + //-----------// + @Override + protected NeuralNetwork unmarshal (InputStream is) + throws JAXBException, IOException + { + return NeuralNetwork.unmarshal(is); + } + + //---------------// + // createNetwork // + //---------------// + private NeuralNetwork createNetwork () + { + // Note : We allocate a hidden layer with as many cells as the output + // layer + NeuralNetwork nn = new NeuralNetwork( + ShapeDescription.length(), + shapeCount, + shapeCount, + getAmplitude(), + ShapeDescription.getParameterLabels(), // Input labels + ShapeSet.getPhysicalShapeNames(), // Output labels + getLearningRate(), + getMomentum(), + getMaxError(), + getListEpochs()); + + return nn; + } + + //~ Inner Classes ---------------------------------------------------------- + private static final class Constants + extends ConstantSet + { + //~ Instance fields ---------------------------------------------------- + + Constant.Ratio amplitude = new Constant.Ratio( + 0.5, + "Initial weight amplitude"); + + Constant.Ratio learningRate = new Constant.Ratio( + 0.2, + "Learning Rate"); + + Constant.Integer listEpochs = new Constant.Integer( + "Epochs", + 4000, + "Number of epochs for training on list of glyphs"); + + Constant.Integer quorum = new Constant.Integer( + "Glyphs", + 10, + "Minimum number of glyphs for each shape"); + + Evaluation.Grade maxError = new Evaluation.Grade( + 1E-3, + "Threshold to stop training"); + + Constant.Ratio momentum = new Constant.Ratio(0.2, "Training momentum"); + + } +} diff --git a/src/main/omr/glyph/GlyphRegression.java b/src/main/omr/glyph/GlyphRegression.java new file mode 100644 index 0000000..d1b24fd --- /dev/null +++ b/src/main/omr/glyph/GlyphRegression.java @@ -0,0 +1,875 @@ +//----------------------------------------------------------------------------// +// // +// G l y p h R e g r e s s i o n // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.glyph; + +import omr.Main; + +import omr.constant.Constant; +import omr.constant.ConstantSet; + +import omr.glyph.facets.Glyph; + +import omr.math.LinearEvaluator; +import omr.math.LinearEvaluator.Sample; + +import org.jdesktop.application.Application.ExitListener; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.EnumMap; +import java.util.EventObject; +import java.util.List; + +import javax.xml.bind.JAXBException; + +/** + * Class {@code GlyphRegression} is a glyph evaluator that encapsulates + * a {@link LinearEvaluator} working on glyph parameters. + * + * @author Hervé Bitteur + */ +public class GlyphRegression + extends AbstractEvaluationEngine +{ + //~ Static fields/initializers --------------------------------------------- + + /** Specific application parameters */ + private static final Constants constants = new Constants(); + + /** Usual logger utility */ + private static final Logger logger = LoggerFactory.getLogger( + GlyphRegression.class); + + /** LinearEvaluator backup file name */ + private static final String BACKUP_FILE_NAME = "linear-evaluator.xml"; + + /** The singleton */ + private static volatile GlyphRegression INSTANCE; + + //~ Instance fields -------------------------------------------------------- + /** The encapsulated linear evaluator */ + private LinearEvaluator engine; + + /** The constraints (minimum, maximum) per shape & per parameter */ + private EnumMap constraintMap = new EnumMap<>( + Shape.class); + + //~ Constructors ----------------------------------------------------------- + //-----------------// + // GlyphRegression // + //-----------------// + /** + * Private constructor + */ + private GlyphRegression () + { + // Unmarshal from backup data + engine = (LinearEvaluator) unmarshal(); + + if (engine == null) { + // Get a brand new one (not trained) + logger.info("Creating a brand new {}", getName()); + engine = new LinearEvaluator(ShapeDescription.getParameterLabels()); + } else { + defineConstraints(); + + // debug + // for (Shape shape : ShapeSet.allPhysicalShapes) { + // dumpOneShapeConstraints(shape); + // } + } + + // Listen to application exit + if (Main.getGui() != null) { + Main.getGui().addExitListener( + new ExitListener() + { + @Override + public boolean canExit (EventObject eo) + { + return true; + } + + @Override + public void willExit (EventObject eo) + { + if (engine.isDataModified()) { + marshal(); + } + } + }); + } + } + + //~ Methods ---------------------------------------------------------------- + // //--------------------// + // // constraintsMatched // + // //--------------------// + // /** + // * Check that all the (non-disabled) constraints matched between a + // * given glyph and a shape + // * @param params the glyph features + // * @param eval the evaluation context to update + // * @return true if matched, false otherwise + // */ + // public boolean constraintsMatched (double[] params, + // Evaluation eval) + // { + // String failed = firstMisMatched(params, eval.shape); + // + // if (failed != null) { + // eval.failure = new Evaluation.Failure(failed); + // + // return false; + // } else { + // return true; + // } + // } + //--------------// + // isCompatible // + //--------------// + @Override + protected final boolean isCompatible (Object obj) + { + if (obj instanceof LinearEvaluator) { + LinearEvaluator anEngine = (LinearEvaluator) obj; + + // Check parameters names, they must be identical + if (!Arrays.equals( + anEngine.getParameterNames(), + ShapeDescription.getParameterLabels())) { + if (logger.isDebugEnabled()) { + logger.debug("Engine parameters: {}", + Arrays.toString(anEngine.getParameterNames())); + logger.debug("Shape parameters: {}", + Arrays.toString(ShapeDescription.getParameterLabels())); + } + return false; + } + + // Check categories names. Order is not relevant + // Engine categories must be a subset of physical shapes + String[] categories = anEngine.getCategoryNames(); + + String[] shapes = ShapeSet.getPhysicalShapeNames(); + String[] sortedShapes = Arrays.copyOf(shapes, shapes.length); + + List extraNames = new ArrayList<>(Arrays.asList(categories)); + extraNames.removeAll(Arrays.asList(sortedShapes)); + + if (!extraNames.isEmpty()) { + if (logger.isDebugEnabled()) { + Arrays.sort(categories); + logger.debug("Engine categories: {}", + Arrays.toString(categories)); + Arrays.sort(sortedShapes); + logger.debug("Physical shapes: {}", + Arrays.toString(sortedShapes)); + logger.debug("Extra names found in {}: {}", + getName(), extraNames); + } + return false; + } else { + return true; + } + } else { + return false; + } + } + + //------// + // dump // + //------// + @Override + public void dump () + { + engine.dump(); + } + + //--------------// + // dumpDistance // + //--------------// + /** + * Print out the "distance" information between a given glyph and a + * shape. It's a sort of debug information. + * + * @param glyph the glyph at hand + * @param shape the shape to measure distance from + */ + public void dumpDistance (Glyph glyph, + Shape shape) + { + // shapeDescs[shape.ordinal()].dumpDistance(glyph); + } + + //-------------------------// + // dumpOneShapeConstraints // + //-------------------------// + public void dumpOneShapeConstraints (Shape shape) + { + StringBuilder sb = new StringBuilder(); + + sb.append(String.format("Constraints for %s: ", shape)); + + String[] labels = ShapeDescription.getParameterLabels(); + Range[] ranges = constraintMap.get(shape); + + if (ranges == null) { + sb.append(String.format("none%n")); + } else { + sb.append(String.format("%n")); + + for (int p = 0; p < ShapeDescription.length(); p++) { + StringBuilder sbp = new StringBuilder(); + Range range = ranges[p]; + + if (range != null) { + Double min = range.min; + + if (min != null) { + sbp.append(" min=").append(min); + } + + Double max = range.max; + + if (max != null) { + sbp.append(" max=").append(max); + } + } + + if (sbp.length() > 0) { + sb.append(String.format(" %s:%s%n", labels[p], sbp)); + } + } + } + + logger.info(sb.toString()); + } + + //-----------// + // getEngine // + //-----------// + /** + * @return the engine + */ + public LinearEvaluator getEngine () + { + return engine; + } + + //-------------// + // getInstance // + //-------------// + /** + * Provide access to the single instance of GlyphRegression for the + * application + * + * @return the GlyphRegression instance + */ + public static GlyphRegression getInstance () + { + if (INSTANCE == null) { + synchronized (GlyphRegression.class) { + if (INSTANCE == null) { + INSTANCE = new GlyphRegression(); + } + } + } + + return INSTANCE; + } + + //------------// + // getMaximum // + //------------// + /** + * Get the constraint test on maximum for a parameter of the + * provided shape. + * + * @param paramIndex the impacted parameter + * @param shape the targeted shape + * @return the current maximum value (null if test is disabled) + */ + public Double getMaximum (int paramIndex, + Shape shape) + { + Range[] ranges = constraintMap.get(shape); + + if (ranges == null) { + return null; + } + + Range range = ranges[paramIndex]; + + return (range != null) ? range.max : null; + } + + //------------// + // getMinimum // + //------------// + /** + * Get the constraint test on minimum for a parameter of the + * provided shape. + * + * @param paramIndex the impacted parameter + * @param shape the targeted shape + * @return the current minimum value (null if test is disabled) + */ + public Double getMinimum (int paramIndex, + Shape shape) + { + Range[] ranges = constraintMap.get(shape); + + if (ranges == null) { + return null; + } + + Range range = ranges[paramIndex]; + + return (range != null) ? range.min : null; + } + + //---------// + // getName // + //---------// + @Override + public final String getName () + { + return "Linear Evaluator"; + } + + //---------------// + // includeSample // + //---------------// + /** + * Take into account the observed parameters for the provided shape, + * and relax the related constraints if needed. + * + * @param params the observed input parameters + * @param shape the provided shape + * @return true if constraints have been extended + */ + public boolean includeSample (double[] params, + Shape shape) + { + // Include this observation + boolean extended = engine.includeSample(params, shape.toString()); + + if (extended) { + // Update extended constraints for the shape + defineOneShapeConstraints(shape); + } + + return extended; + } + + //-----------------// + // measureDistance // + //-----------------// + /** + * Measure the "distance" information between a given glyph and a + * shape. + * + * @param glyph the glyph at hand + * @param shape the shape to measure distance from + * @return the measured distance + */ + public double measureDistance (Glyph glyph, + Shape shape) + { + return engine.categoryDistance( + ShapeDescription.features(glyph), + shape.toString()); + } + + //-----------------// + // measureDistance // + //-----------------// + /** + * Measure the "distance" information between a given glyph and a + * shape. + * + * @param ins the input parameters + * @param shape the shape to measure distance from + * @return the measured distance + */ + public double measureDistance (double[] ins, + Shape shape) + { + return engine.categoryDistance(ins, shape.toString()); + } + + //-----------------// + // measureDistance // + //-----------------// + /** + * Measure the "distance" information between two glyphs. + * + * @param one the first glyph + * @param two the second glyph + * @return the measured distance + */ + public double measureDistance (Glyph one, + Glyph two) + { + return measureDistance(one, ShapeDescription.features(two)); + } + + //-----------------// + // measureDistance // + //-----------------// + /** + * Measure the "distance" information between a glyph and an array + * of parameters (generally fed from another glyph). + * + * @param glyph the given glyph + * @param ins the array (size = paramCount) of parameters + * @return the measured distance + */ + public double measureDistance (Glyph glyph, + double[] ins) + { + return engine.patternDistance(ShapeDescription.features(glyph), ins); + } + + //------------// + // setMaximum // + //------------// + /** + * Set the constraint test on maximum for a parameter of the + * provided shape. + * + * @param paramIndex the impacted parameter + * @param shape the targeted shape + * @param val the new maximum value (null for disabling the test) + */ + public void setMaximum (int paramIndex, + Shape shape, + Double val) + { + doGetRange(paramIndex, shape).max = val; + } + + //------------// + // setMinimum // + //------------// + /** + * Set the constraint test on minimum for a parameter of the + * provided shape. + * + * @param paramIndex the impacted parameter + * @param shape the targeted shape + * @param val the new minimum value (null for disabling the test) + */ + public void setMinimum (int paramIndex, + Shape shape, + Double val) + { + doGetRange(paramIndex, shape).min = val; + } + + //-------// + // train // + //-------// + /** + * Launch the training of the evaluator. + * + * @param base the collection of glyphs used for training + * @param monitor a monitoring entity + * @param mode incremental or scratch mode + */ + @Override + public void train (Collection base, + Monitor monitor, + StartingMode mode) + { + if (base.isEmpty()) { + logger.warn("No glyph to retrain Regression Evaluator"); + + return; + } + + // Prepare the collection of samples + Collection samples = new ArrayList<>(); + + for (Glyph glyph : base) { + try { + Shape shape = glyph.getShape().getPhysicalShape(); + Sample sample = new Sample( + shape.toString(), + ShapeDescription.features(glyph)); + samples.add(sample); + } catch (Exception ex) { + logger.warn( + "Weird glyph shape: " + glyph.getShape() + " file=" + + GlyphRepository.getInstance().getGlyphName(glyph), + ex); + } + } + + // Do the training + engine.train(samples); + + // Save to disk + marshal(); + } + + //-------------// + // getFileName // + //-------------// + @Override + protected String getFileName () + { + return BACKUP_FILE_NAME; + } + + //-------------------// + // getRawEvaluations // + //-------------------// + @Override + protected Evaluation[] getRawEvaluations (Glyph glyph) + { + // If too small, it's just NOISE + if (!isBigEnough(glyph)) { + return noiseEvaluations; + } else { + double[] ins = ShapeDescription.features(glyph); + Evaluation[] evals = new Evaluation[shapeCount]; + Shape[] values = Shape.values(); + + for (int s = 0; s < shapeCount; s++) { + Shape shape = values[s]; + evals[s] = new Evaluation( + shape, + 1d / measureDistance(ins, shape)); + } + + // Order the evals from best to worst + Arrays.sort(evals); + + return evals; + } + } + + //---------// + // marshal // + //---------// + @Override + protected void marshal (OutputStream os) + throws FileNotFoundException, IOException, JAXBException + { + engine.marshal(os); + } + + //-----------// + // unmarshal // + //-----------// + @Override + protected LinearEvaluator unmarshal (InputStream is) + throws JAXBException + { + return LinearEvaluator.unmarshal(is); + } + + //-------------------// + // defineConstraints // + //-------------------// + /** + * Here we customize the linear evaluator to our specific needs, + * by removing some constraint checks and relaxing others. + */ + private void defineConstraints () + { + for (Shape shape : ShapeSet.allPhysicalShapes) { + defineOneShapeConstraints(shape); + } + } + + //---------------------------// + // defineOneShapeConstraints // + //---------------------------// + /** + * Here we customize the constraints to our specific needs, by + * removing some constraint checks and relaxing others. + * + * @param shape the shape at hand + */ + private void defineOneShapeConstraints (Shape shape) + { + // // First, use LinearEvaluator observed constraints + // for (int p = 0; p < ShapeDescription.length(); p++) { + // setMinimum(p, shape, engine.getMinimum(p, shape.name())); + // setMaximum(p, shape, engine.getMaximum(p, shape.name())); + // } + // + // // Second, relax some constraints + // // Add some margin around constraints + // double minFactor = constants.factorForMinima.getValue(); + // double maxFactor = constants.factorForMaxima.getValue(); + // + // for (String label : Arrays.asList("weight", "width", "height")) { + // int p = ShapeDescription.getParameterIndex(label); + // Double val = getMinimum(p, shape); + // + // if (val != null) { + // if (val > 0) { + // setMinimum(p, shape, val * minFactor); + // } else { + // setMinimum(p, shape, val * maxFactor); + // } + // } + // + // val = getMaximum(p, shape); + // + // if (val != null) { + // if (val > 0) { + // setMaximum(p, shape, val * maxFactor); + // } else { + // setMaximum(p, shape, val * minFactor); + // } + // } + // } + // + // // Disable some selected features + // for (String label : Arrays.asList( + // "ledger", + // "n11", + // "n20", + // "n02", + // "n30", + // "n21", + // "n12", + // "n03", + // "aspect")) { + // int p = ShapeDescription.getParameterIndex(label); + // setMinimum(p, shape, null); + // setMaximum(p, shape, null); + // } + // + // // Keep "stemNb" exactly as it is, with no margin + // + // // Third, remove some constraints + // switch (shape) { + // case TEXT : + // disableMaximum(TEXT, "weight"); + // disableMaximum(TEXT, "width"); + // + // break; + // + // case BRACE : + // disableMaximum(BRACE, "weight"); + // disableMaximum(BRACE, "height"); + // + // break; + // + // case BRACKET : + // disableMaximum(BRACKET, "weight"); + // disableMaximum(BRACKET, "height"); + // + // break; + // + // case BEAM : + // disableMaximum(BEAM, "weight"); + // disableMaximum(BEAM, "width"); + // disableMaximum(BEAM, "height"); + // + // break; + // + // case BEAM_2 : + // disableMaximum(BEAM_2, "weight"); + // disableMaximum(BEAM_2, "width"); + // disableMaximum(BEAM_2, "height"); + // + // break; + // + // case BEAM_3 : + // disableMaximum(BEAM_3, "weight"); + // disableMaximum(BEAM_3, "width"); + // disableMaximum(BEAM_3, "height"); + // + // break; + // + // case SLUR : + // disableMaximum(SLUR, "weight"); + // disableMaximum(SLUR, "width"); + // disableMaximum(SLUR, "height"); + // + // break; + // + // case ARPEGGIATO : + // disableMaximum(ARPEGGIATO, "weight"); + // disableMaximum(ARPEGGIATO, "height"); + // + // break; + // + // case CRESCENDO : + // disableMaximum(CRESCENDO, "weight"); + // disableMaximum(CRESCENDO, "width"); + // disableMaximum(CRESCENDO, "height"); + // + // break; + // + // case DECRESCENDO : + // disableMaximum(DECRESCENDO, "weight"); + // disableMaximum(DECRESCENDO, "width"); + // disableMaximum(DECRESCENDO, "height"); + // + // break; + // + // default : + // } + } + + //----------------// + // disableMaximum // + //----------------// + private void disableMaximum (Shape shape, + String paramLabel) + { + setMaximum(ShapeDescription.getParameterIndex(paramLabel), shape, null); + } + + //----------------// + // disableMinimum // + //----------------// + private void disableMinimum (Shape shape, + String paramLabel) + { + setMinimum(ShapeDescription.getParameterIndex(paramLabel), shape, null); + } + + //------------// + // doGetRange // + //------------// + /** + * Retrieve (and create if necessary) the range entity that + * corresponds to parameter of paramIndex for the provided shape. + * + * @param paramIndex parameter reference + * @param shape provided shape + * @return the desired range entity + */ + private Range doGetRange (int paramIndex, + Shape shape) + { + Range[] ranges = constraintMap.get(shape); + + if (ranges == null) { + ranges = new Range[ShapeDescription.length()]; + constraintMap.put(shape, ranges); + } + + Range range = ranges[paramIndex]; + + if (range == null) { + ranges[paramIndex] = range = new Range(); + } + + return range; + } + + //-----------------// + // firstMisMatched // + //-----------------// + /** + * Perform a basic check on max / min bounds, if any, for each + * parameter value of the provided pattern. + * + * @param pattern the collection of parameters to check with respect to + * targeted shape + * @param shape the targeted shape + * @return the name of the first failing check, null otherwise + */ + private String firstMisMatched (double[] pattern, + Shape shape) + { + String[] labels = ShapeDescription.getParameterLabels(); + Range[] ranges = constraintMap.get(shape); + + if (ranges == null) { + return null; // No test => OK + } + + for (int p = 0; p < ShapeDescription.length(); p++) { + String label = labels[p]; + double val = pattern[p]; + Range range = ranges[p]; + + if (range == null) { + continue; + } + + Double min = range.min; + + if ((min != null) && (val < min)) { + logger.debug("{} failed on minimum for {} {} < {}", + shape, label, val, min); + return label + ".min"; + } + + Double max = range.max; + + if ((max != null) && (val > max)) { + logger.debug("{} failed on maximum for {} {} > {}", + shape, label, val, max); + return label + ".max"; + } + } + + // Everything is OK + return null; + } + + //~ Inner Classes ---------------------------------------------------------- + //-------// + // Range // + //-------// + public static class Range + { + //~ Instance fields ---------------------------------------------------- + + /** Constraint on minimum, if any */ + Double min; + + /** Constraint on maximum, if any */ + Double max; + + } + + //-----------// + // Constants // + //-----------// + private static final class Constants + extends ConstantSet + { + //~ Instance fields ---------------------------------------------------- + + Constant.Double factorForMinima = new Constant.Double( + "factor", + 0.7, + "Factor applied to all minimum constraints"); + + Constant.Double factorForMaxima = new Constant.Double( + "factor", + 1.3, + "Factor applied to all maximum constraints"); + + } +} diff --git a/src/main/omr/glyph/GlyphRepository.java b/src/main/omr/glyph/GlyphRepository.java new file mode 100644 index 0000000..4994720 --- /dev/null +++ b/src/main/omr/glyph/GlyphRepository.java @@ -0,0 +1,1071 @@ +//----------------------------------------------------------------------------// +// // +// G l y p h R e p o s i t o r y // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.glyph; + +import omr.WellKnowns; + +import omr.glyph.facets.BasicGlyph; +import omr.glyph.facets.Glyph; +import omr.glyph.facets.GlyphValue; + +import omr.lag.Section; + +import omr.sheet.Sheet; + +import omr.ui.symbol.MusicFont; +import omr.ui.symbol.ShapeSymbol; +import omr.ui.symbol.Symbols; + +import omr.util.BlackList; +import omr.util.FileUtil; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.io.FileFilter; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; + +import javax.xml.bind.JAXBContext; +import javax.xml.bind.JAXBException; +import javax.xml.bind.Marshaller; +import javax.xml.bind.Unmarshaller; + +/** + * Class {@code GlyphRepository} handles the store of known glyphs, + * across multiple sheets (and possibly multiple runs). + * + *

A glyph is known by its full name, whose standard format is + * sheetName/Shape.id.xml, regardless of the area it is stored (this may + * be the core area or the global sheets area augmented by the + * samples area). + * It can also be an artificial glyph built from a symbol icon, + * in that case its full name is the similar formats icons/Shape.xml or + * icons/Shape.nn.xml where "nn" is a differentiating number. + * + *

The repository handles a private map of all deserialized glyphs so far, + * since the deserialization is a rather expensive operation. + * + *

It handles two bases : the "whole base" (all glyphs from sheets and + * samples folders) and the "core base" (just the glyphs of the core, which is + * built as a selected subset of the whole base). + * These bases are accessible respectively by {@link #getWholeBase} and + * {@link #getCoreBase} methods. + * + * @author Hervé Bitteur + */ +public class GlyphRepository +{ + //~ Static fields/initializers --------------------------------------------- + + /** Usual logger utility */ + private static final Logger logger = LoggerFactory.getLogger( + GlyphRepository.class); + + /** The single instance of this class */ + private static volatile GlyphRepository INSTANCE; + + /** Extension for training files */ + private static final String FILE_EXTENSION = ".xml"; + + /** Extension for place-holder symbol files */ + public static final String SYMBOL_EXTENSION = ".symbol"; + + /** Specific subdirectory for sheet glyphs */ + private static final File sheetsFolder = new File( + WellKnowns.TRAIN_FOLDER, + "sheets"); + + /** Specific subdirectory for core glyphs */ + private static final File coreFolder = new File( + WellKnowns.TRAIN_FOLDER, + "core"); + + /** Specific subdirectory for additional sample glyphs */ + private static final File samplesFolder = new File( + WellKnowns.TRAIN_FOLDER, + "samples"); + + /** Specific filter for glyph files */ + private static final FileFilter glyphFilter = new FileFilter() + { + @Override + public boolean accept (File file) + { + String ext = FileUtil.getExtension(file); + + return file.isDirectory() || ext.equals(FILE_EXTENSION) + || ext.equals(SYMBOL_EXTENSION); + } + }; + + /** Un/marshalling context for use with JAXB */ + private static volatile JAXBContext jaxbContext; + + /** For comparing shape names */ + public static final Comparator shapeComparator = new Comparator() + { + @Override + public int compare (String s1, + String s2) + { + String n1 = GlyphRepository.shapeNameOf(s1); + String n2 = GlyphRepository.shapeNameOf(s2); + + return n1.compareTo(n2); + } + }; + + //~ Instance fields -------------------------------------------------------- + /** Core collection of glyphs */ + private volatile List coreBase; + + /** Whole collection of glyphs */ + private volatile List wholeBase; + + /** + * Map of all glyphs deserialized so far, using full glyph name as + * key. Full glyph name format is : sheetName/Shape.id.xml + */ + private final Map glyphsMap = new TreeMap<>(); + + /** Inverse map */ + private final Map namesMap = new HashMap<>(); + + //~ Constructors ----------------------------------------------------------- + /** Private singleton constructor */ + private GlyphRepository () + { + } + + //~ Methods ---------------------------------------------------------------- + //------------// + // fileNameOf // + //------------// + /** + * Report the file name w/o extension of a gName. + * + * @param gName glyph name, using format "folder/name.number.xml" + * or "folder/name.xml" + * @return the 'name' or 'name.number' part of the format + */ + public static String fileNameOf (String gName) + { + int slash = gName.indexOf('/'); + String nameWithExt = gName.substring(slash + 1); + + int lastDot = nameWithExt.lastIndexOf('.'); + + if (lastDot != -1) { + return nameWithExt.substring(0, lastDot); + } else { + return nameWithExt; + } + } + + //-------------// + // shapeNameOf // + //-------------// + /** + * Report the shape name of a gName. + * + * @param gName glyph name, using format "folder/name.number.xml" or + * "folder/name.xml" + * @return the 'name' part of the format + */ + public static String shapeNameOf (String gName) + { + int slash = gName.indexOf('/'); + String nameWithExt = gName.substring(slash + 1); + + int firstDot = nameWithExt.indexOf('.'); + + if (firstDot != -1) { + return nameWithExt.substring(0, firstDot); + } else { + return nameWithExt; + } + } + + //-------------// + // getCoreBase // + //-------------// + /** + * Return the names of the core collection of glyphs. + * + * @return the core collection of recorded glyphs + */ + public List getCoreBase (Monitor monitor) + { + if (coreBase == null) { + synchronized (this) { + if (coreBase == null) { + coreBase = loadCoreBase(monitor); + } + } + } + + return coreBase; + } + + //----------// + // getGlyph // + //----------// + /** + * Return a glyph knowing its full glyph name, which is the name of + * the corresponding training material. + * If not already done, the glyph is deserialized from the training file, + * searching first in the icons area, then the train area. + * + * @param gName the full glyph name (format is: sheetName/Shape.id.xml) + * @param monitor the monitor, if any, to be kept informed of glyph loading + * @return the glyph instance if found, null otherwise + */ + public synchronized Glyph getGlyph (String gName, + Monitor monitor) + { + // First, try the map of glyphs + Glyph glyph = glyphsMap.get(gName); + + if (glyph == null) { + // If failed, actually load the glyph from XML backup file. + if (isIcon(gName)) { + glyph = buildSymbolGlyph(gName); + } else { + File file = new File(WellKnowns.TRAIN_FOLDER, gName); + + if (!file.exists()) { + logger.warn("Unable to find file for glyph {}", gName); + + return null; + } + + glyph = buildGlyph(gName, file); + } + + if (glyph != null) { + glyphsMap.put(gName, glyph); + namesMap.put(glyph, gName); + } + + if (monitor != null) { + monitor.loadedGlyph(gName); + } + } + + return glyph; + } + + //--------------// + // getGlyphName // + //--------------// + public String getGlyphName (Glyph glyph) + { + return namesMap.get(glyph); + } + + //-------------// + // getGlyphsIn // + //-------------// + /** + * Report the list of glyph files that are contained within a given + * directory + * + * @param dir the containing directory + * @return the list of glyph files + */ + public synchronized List getGlyphsIn (File dir) + { + File[] files = listLegalFiles(dir); + + if (files != null) { + return Arrays.asList(files); + } else { + logger.warn("Cannot get files list from dir {}", dir); + + return new ArrayList<>(); + } + } + + //-------------// + // getInstance // + //-------------// + /** + * Report the single instance of this class, after creating it if + * needed. + * + * @return the single instance + */ + public static GlyphRepository getInstance () + { + if (INSTANCE == null) { + INSTANCE = new GlyphRepository(); + } + + return INSTANCE; + } + + //----------------------// + // getSampleDirectories // + //----------------------// + /** + * Report the list of all samples directories found in the training + * material. + * + * @return the list of samples directories + */ + public List getSampleDirectories () + { + return getSubdirectories(samplesFolder); + } + + //------------------// + // getSamplesFolder // + //------------------// + /** + * Report the folder where isolated samples glyphs are stored. + * + * @return the directory of isolated samples material + */ + public File getSamplesFolder () + { + return samplesFolder; + } + + //---------------------// + // getSheetDirectories // + //---------------------// + /** + * Report the list of all sheet directories found in the training + * material. + * + * @return the list of sheet directories + */ + public List getSheetDirectories () + { + return getSubdirectories(sheetsFolder); + } + + //-----------------// + // getSheetsFolder // + //-----------------// + /** + * Report the folder where all sheet glyphs are stored. + * + * @return the directory of all sheets material + */ + public File getSheetsFolder () + { + return sheetsFolder; + } + + //--------------// + // getWholeBase // + //--------------// + /** + * Return the names of the whole collection of glyphs. + * + * @return the whole collection of recorded glyphs + */ + public List getWholeBase (Monitor monitor) + { + if (wholeBase == null) { + synchronized (this) { + if (wholeBase == null) { + wholeBase = loadWholeBase(monitor); + } + } + } + + return wholeBase; + } + + //--------// + // isIcon // + //--------// + public boolean isIcon (String gName) + { + return isIcon(new File(gName)); + } + + //---------------// + // isIconsFolder // + //---------------// + public boolean isIconsFolder (String folder) + { + return folder.equals(WellKnowns.SYMBOLS_FOLDER.getName()); + } + + //----------// + // isLoaded // + //----------// + public synchronized boolean isLoaded (String gName) + { + return glyphsMap.get(gName) != null; + } + + //----------------// + // recordOneGlyph // + //----------------// + /** + * Record one glyph on disk (into the samples folder). + * + * @param glyph the glyph to record + * @param sheet its containing sheet + */ + public void recordOneGlyph (Glyph glyph, + Sheet sheet) + { + Shape shape = getRecordableShape(glyph); + + if (shape != null) { + // Prepare target directory, based on sheet id + File sheetDir = new File(getSamplesFolder(), sheet.getId()); + + // Make sure related directory chain exists + if (sheetDir.mkdirs()) { + logger.info("Creating directory {}", sheetDir); + } + + if (recordGlyph(glyph, shape, sheetDir) > 0) { + logger.info("Stored {} into {}", glyph.idString(), sheetDir); + } + } else { + logger.warn("Not recordable {}", glyph); + } + } + + //-------------------// + // recordSheetGlyphs // + //-------------------// + /** + * Store all known glyphs of the provided sheet as separate XML + * files, so that they can be later reloaded to train an evaluator. + * We store glyph for which Shape is not null, and different from NOISE and + * STEM (CLUTTER is thus stored as well). + * + *

STRUCTURE shapes are stored in a parallel sub-directory so that they + * don't get erased by shapes of their leaves. + * + * @param sheet the sheet whose glyphs are to be stored + * @param emptyStructures flag to specify if the Structure directory must be + * emptied beforehand + */ + public void recordSheetGlyphs (Sheet sheet, + boolean emptyStructures) + { + // Prepare target directory + File sheetDir = new File(getSheetsFolder(), sheet.getId()); + + // Make sure related directory chain exists + if (sheetDir.mkdirs()) { + logger.info("Creating directory {}", sheetDir); + } else { + deleteXmlFiles(sheetDir); + } + + // Now record each relevant glyph + int glyphNb = 0; + + for (Glyph glyph : sheet.getActiveGlyphs()) { + Shape shape = getRecordableShape(glyph); + + if (shape != null) { + glyphNb += recordGlyph(glyph, shape, sheetDir); + } + } + + // Refresh glyph populations + refreshBases(); + + logger.info("{} glyphs stored from {}", glyphNb, sheet.getId()); + } + + //--------------// + // refreshBases // + //--------------// + public void refreshBases () + { + wholeBase = null; + coreBase = null; + } + + //-------------// + // removeGlyph // + //-------------// + /** + * Remove a glyph from the repository memory (this does not delete + * the actual glyph file on disk). + * We also remove it from the various bases which is safer. + * + * @param gName the full glyph name + */ + public synchronized void removeGlyph (String gName) + { + glyphsMap.remove(gName); + refreshBases(); + } + + //-------------// + // setCoreBase // + //-------------// + /** + * Define the provided collection as the core training material. + * + * @param base the provided collection + */ + public synchronized void setCoreBase (List base) + { + coreBase = base; + } + + //---------// + // shapeOf // + //---------// + /** + * Infer the shape of a glyph directly from its full name. + * + * @param gName the full glyph name + * @return the shape of the known glyph + */ + public Shape shapeOf (String gName) + { + return shapeOf(new File(gName)); + } + + //---------------// + // storeCoreBase // + //---------------// + /** + * Store the core training material. + */ + public synchronized void storeCoreBase () + { + if (coreBase == null) { + logger.warn("Core base is null"); + + return; + } + + // Create the core directory if needed + coreFolder.mkdirs(); + + // Empty the directory + FileUtil.deleteAll(coreFolder.listFiles()); + + // Copy the glyph and icon files into the core directory + int copyNb = 0; + + for (String gName : coreBase) { + final boolean isIcon = isIcon(gName); + final File source = isIcon + ? new File( + WellKnowns.SYMBOLS_FOLDER.getParentFile(), + gName) : new File(WellKnowns.TRAIN_FOLDER, gName); + + final File target = new File(coreFolder, gName); + target.getParentFile().mkdirs(); + + logger.debug("Storing {} as core", target); + + try { + if (isIcon) { + target.createNewFile(); + } else { + FileUtil.copy(source, target); + } + + copyNb++; + } catch (IOException ex) { + logger.warn("Cannot copy {} to {}", source, target); + } + } + + logger.info("{} glyphs copied as core training material", copyNb); + } + + //-----------------// + // unloadIconsFrom // + //-----------------// + public void unloadIconsFrom (List names) + { + for (String gName : names) { + if (isIcon(gName)) { + if (isLoaded(gName)) { + Glyph glyph = getGlyph(gName, null); + + for (Section section : glyph.getMembers()) { + section.clearViews(); + section.delete(); + } + } + + unloadGlyph(gName); + } + } + } + + //-------------// + // unloadGlyph // + //-------------// + synchronized void unloadGlyph (String gName) + { + if (glyphsMap.containsKey(gName)) { + glyphsMap.remove(gName); + } + } + + //------------// + // buildGlyph // + //------------// + private Glyph buildGlyph (String gName, + File file) + { + logger.debug("Loading glyph {}", file); + + Glyph glyph = null; + InputStream is = null; + + try { + is = new FileInputStream(file); + glyph = jaxbUnmarshal(is); + } catch (Exception ex) { + logger.warn("Could not unmarshal file {}", file); + ex.printStackTrace(); + } finally { + if (is != null) { + try { + is.close(); + } catch (Exception ignored) { + } + } + } + + return glyph; + } + + //------------------// + // buildSymbolGlyph // + //------------------// + /** + * Build an artificial glyph from a symbol descriptor, in order to + * train an evaluator even when we have no ground-truth glyph. + * + * @param gName path to the symbol descriptor on disk + * @return the glyph built, or null if failed + */ + private Glyph buildSymbolGlyph (String gName) + { + Shape shape = shapeOf(gName); + Glyph glyph = null; + + // Make sure we have the drawing available for this shape + ShapeSymbol symbol = Symbols.getSymbol(shape); + + // If no plain symbol, use the decorated symbol as plan B + if (symbol == null) { + symbol = Symbols.getSymbol(shape, true); + } + + if (symbol != null) { + logger.debug("Building symbol glyph {}", gName); + + File file = new File(WellKnowns.TRAIN_FOLDER, gName); + + if (file.exists()) { + try { + InputStream is = new FileInputStream(file); + SymbolGlyphDescriptor desc = SymbolGlyphDescriptor. + loadFromXmlStream( + is); + is.close(); + + logger.debug("Descriptor {}", desc); + + glyph = new SymbolGlyph( + shape, + symbol, + MusicFont.DEFAULT_INTERLINE, + desc); + } catch (Exception ex) { + logger.warn("Cannot process " + file, ex); + } + } + } else { + //if (logger.isDebugEnabled()) { + logger.warn("No symbol for {}", gName); + + //} + } + + return glyph; + } + + //----------------// + // deleteXmlFiles // + //----------------// + private void deleteXmlFiles (File dir) + { + File[] files = dir.listFiles(); + + for (File file : files) { + if (FileUtil.getExtension(file).equals(FILE_EXTENSION)) { + if (!file.delete()) { + logger.warn("Could not delete {}", file); + } + } + } + } + + //----------------// + // getJaxbContext // + //----------------// + private JAXBContext getJaxbContext () + throws JAXBException + { + // Lazy creation + if (jaxbContext == null) { + jaxbContext = JAXBContext.newInstance(GlyphValue.class); + } + + return jaxbContext; + } + + //--------------------// + // getRecordableShape // + //--------------------// + /** + * Report the shape to record for the provided glyph. + * + * @param glyph the provided glyph + * @return the precise shape to use, or null + */ + private Shape getRecordableShape (Glyph glyph) + { + if ((glyph == null) || glyph.isVirtual() || (glyph.getShape() == null)) { + return null; + } + + Shape shape = glyph.getShape().getPhysicalShape(); + + if (shape.isTrainable() && (shape != Shape.NOISE)) { + return shape; + } else { + return null; + } + } + + //-------------------// + // getSubdirectories // + //-------------------// + private synchronized List getSubdirectories (File folder) + { + List dirs = new ArrayList<>(); + File[] files = listLegalFiles(folder); + + for (File file : files) { + if (file.isDirectory()) { + dirs.add(file); + } + } + + return dirs; + } + + //-------------// + // glyphNameOf // + //-------------// + /** + * Build the full glyph name (which will be the unique glyph name) + * from the file which contains the glyph description. + * + * @param file the glyph backup file + * @return the unique glyph name + */ + private String glyphNameOf (File file) + { + if (isIcon(file)) { + return file.getParentFile().getName() + File.separator + file. + getName(); + } else { + return file.getParentFile().getParentFile().getName() + File.separator + + file.getParentFile().getName() + File.separator + file. + getName(); + } + } + + //--------// + // isIcon // + //--------// + private boolean isIcon (File file) + { + String folder = file.getParentFile().getName(); + + return isIconsFolder(folder); + } + + //-------------// + // jaxbMarshal // + //-------------// + private void jaxbMarshal (Glyph glyph, + OutputStream os) + throws JAXBException, Exception + { + Marshaller m = getJaxbContext().createMarshaller(); + m.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true); + m.marshal(new GlyphValue(glyph), os); + } + + //---------------// + // jaxbUnmarshal // + //---------------// + private Glyph jaxbUnmarshal (InputStream is) + throws JAXBException + { + Unmarshaller um = getJaxbContext().createUnmarshaller(); + GlyphValue value = (GlyphValue) um.unmarshal(is); + + return new BasicGlyph(value); + } + + //----------------// + // listLegalFiles // + //----------------// + private File[] listLegalFiles (File dir) + { + return new BlackList(dir).listFiles(glyphFilter); + } + + //----------// + // loadBase // + //----------// + /** + * Build the map and return the collection of glyphs names in a + * collection of directories. + * + * @param paths the array of paths to the directories to load + * @param monitor the observing entity if any + * @return the collection of loaded glyphs names + */ + private synchronized List loadBase (File[] paths, + Monitor monitor) + { + // Files in the provided directory & its subdirectories + List files = new ArrayList<>(4000); + + for (File path : paths) { + loadDirectory(path, files); + } + + if (monitor != null) { + monitor.setTotalGlyphs(files.size()); + } + + // Now, collect the glyphs names + List base = new ArrayList<>(files.size()); + + for (File file : files) { + base.add(glyphNameOf(file)); + } + + logger.debug("{} glyphs names collected", files.size()); + + return base; + } + + //--------------// + // loadCoreBase // + //--------------// + /** + * Build the collection of only the core glyphs. + * + * @return the collection of core glyphs names + */ + private List loadCoreBase (Monitor monitor) + { + return loadBase(new File[]{coreFolder}, monitor); + } + + //---------------// + // loadDirectory // + //---------------// + /** + * Retrieve recursively all files in the hierarchy starting at + * the given directory, and append them in the provided file list. + * If a black list exists in a directory, then all black-listed files + * (and direct sub-directories) hosted in this directory are skipped. + * + * @param dir the top directory where search is launched + * @param all the list to be augmented by found files + */ + private void loadDirectory (File dir, + List all) + { + File[] files = listLegalFiles(dir); + + logger.debug("Browsing directory {} total:{}", dir, files.length); + + if (files != null) { + for (File file : files) { + if (file.isDirectory()) { + loadDirectory(file, all); // Recurse through it + } else { + all.add(file); + } + } + } else { + logger.warn("Directory {} is empty", dir); + } + } + + //---------------// + // loadWholeBase // + //---------------// + /** + * Build the complete map of all glyphs recorded so far, beginning + * by the builtin icon glyphs, then the recorded glyphs + * (sheets & samples). + * + * @return a collection of (known) glyphs names + */ + private List loadWholeBase (Monitor monitor) + { + return loadBase( + new File[]{WellKnowns.SYMBOLS_FOLDER, sheetsFolder, + samplesFolder}, + monitor); + } + + //-------------// + // recordGlyph // + //-------------// + /** + * Record a glyph, using the precise shape into the given directory. + * + * @param glyph the glyph to record + * @param shape the precise shape to use + * @param dir the target directory + * @return 1 if OK, 0 otherwise + */ + private int recordGlyph (Glyph glyph, + Shape shape, + File dir) + { + OutputStream os = null; + + try { + logger.debug("Storing {}", glyph); + + StringBuilder sb = new StringBuilder(); + sb.append(shape); + sb.append("."); + sb.append(String.format("%04d", glyph.getId())); + sb.append(FILE_EXTENSION); + + File glyphFile; + + glyphFile = new File(dir, sb.toString()); + + os = new FileOutputStream(glyphFile); + jaxbMarshal(glyph, os); + + return 1; + } catch (Throwable ex) { + logger.warn("Error storing " + glyph, ex); + } finally { + try { + if (os != null) { + os.close(); + } + } catch (IOException ex) { + logger.warn(null, ex); + } + } + + return 0; + } + + //---------// + // shapeOf // + //---------// + /** + * Infer the shape of a glyph directly from its file name. + * + * @param file the file that describes the glyph + * @return the shape of the known glyph + */ + private Shape shapeOf (File file) + { + try { + // ex: ONE_32ND_REST.0105.xml (for real glyphs) + // ex: CODA.xml (for glyphs derived from icons) + String name = FileUtil.getNameSansExtension(file); + int dot = name.indexOf('.'); + + if (dot != -1) { + name = name.substring(0, dot); + } + + return Shape.valueOf(name); + } catch (Exception ex) { + // Not recognized + return null; + } + } + + //~ Inner Interfaces ------------------------------------------------------- + //---------// + // Monitor // + //---------// + /** + * Interface {@code Monitor} defines the entries to a UI entity + * which monitors the loading of glyphs by the glyph repository. + */ + public static interface Monitor + { + //~ Methods ------------------------------------------------------------ + + /** + * Called whenever a new glyph has been loaded. + * + * @param gName the normalized glyph name + */ + void loadedGlyph (String gName); + + /** + * Called to pass the number of selected glyphs, which will be + * later loaded. + * + * @param selected the size of the selection + */ + void setSelectedGlyphs (int selected); + + /** + * Called to pass the total number of available glyph + * descriptions in the training material. + * + * @param total the size of the training material + */ + void setTotalGlyphs (int total); + } +} diff --git a/src/main/omr/glyph/GlyphSignature.java b/src/main/omr/glyph/GlyphSignature.java new file mode 100644 index 0000000..d48962a --- /dev/null +++ b/src/main/omr/glyph/GlyphSignature.java @@ -0,0 +1,145 @@ +//----------------------------------------------------------------------------// +// // +// G l y p h S i g n a t u r e // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.glyph; + +import omr.glyph.facets.Glyph; + +import omr.moments.GeometricMoments; + +/** + * Class {@code GlyphSignature} is used to implement a map of glyphs, + * based only on their physical properties. + * + *

The signature is implemented using the glyph moments.

+ * + * @author Hervé Bitteur + */ +public class GlyphSignature + implements Comparable +{ + //~ Instance fields -------------------------------------------------------- + + /** Glyph absolute weight */ + private final int weight; + + /** Glyph normalized moments */ + private GeometricMoments moments; + + //~ Constructors ----------------------------------------------------------- + //----------------// + // GlyphSignature // + //----------------// + /** + * Creates a new GlyphSignature object. + * + * @param glyph the glyph to compute signature upon + */ + public GlyphSignature (Glyph glyph) + { + weight = glyph.getWeight(); + moments = new GeometricMoments(glyph.getGeometricMoments()); + } + + //----------------// + // GlyphSignature // + //----------------// + /** + * Needed by JAXB. + */ + private GlyphSignature () + { + weight = 0; + moments = null; + } + + //~ Methods ---------------------------------------------------------------- + //-----------// + // compareTo // + //-----------// + @Override + public int compareTo (GlyphSignature other) + { + if (weight < other.weight) { + return -1; + } else if (weight > other.weight) { + return 1; + } + + final Double[] values = moments.getValues(); + final Double[] otherValues = other.moments.getValues(); + + for (int i = 0; i < values.length; i++) { + int cmp = Double.compare(values[i], otherValues[i]); + + if (cmp != 0) { + return cmp; + } + } + + return 0; // Equal + } + + //--------// + // equals // + //--------// + @Override + public boolean equals (Object obj) + { + if (obj == this) { + return true; + } + + if (obj instanceof GlyphSignature) { + return compareTo((GlyphSignature) obj) == 0; + } else { + return false; + } + } + + //-----------// + // getWeight // + //-----------// + public int getWeight () + { + return weight; + } + + //----------// + // hashCode // + //----------// + @Override + public int hashCode () + { + int hash = 7; + hash = (41 * hash) + this.weight; + + return hash; + } + + //----------// + // toString // + //----------// + @Override + public String toString () + { + StringBuilder sb = new StringBuilder("{GSig"); + + sb.append(" weight=") + .append(weight); + + sb.append(" moments=") + .append(moments); + sb.append("}"); + + return sb.toString(); + } +} diff --git a/src/main/omr/glyph/Glyphs.java b/src/main/omr/glyph/Glyphs.java new file mode 100644 index 0000000..826788c --- /dev/null +++ b/src/main/omr/glyph/Glyphs.java @@ -0,0 +1,663 @@ +//----------------------------------------------------------------------------// +// // +// G l y p h s // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.glyph; + +import omr.glyph.facets.BasicAlignment; +import omr.glyph.facets.Glyph; + +import omr.lag.Section; + +import omr.math.PointsCollector; + +import omr.run.Orientation; + +import omr.sheet.Scale; + +import omr.util.Predicate; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.awt.Polygon; +import java.awt.Rectangle; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.EnumSet; +import java.util.Iterator; +import java.util.LinkedHashSet; +import java.util.Set; +import java.util.SortedSet; +import java.util.TreeSet; + +/** + * Class {@code Glyphs} is a collection of static convenient methods, + * providing features related to a collection of glyphs. + * + * @author Hervé Bitteur + */ +public class Glyphs +{ + //~ Static fields/initializers --------------------------------------------- + + /** Usual logger utility */ + private static final Logger logger = LoggerFactory.getLogger(Glyphs.class); + + /** Predicate to check for a manual shape */ + public static final Predicate manualPredicate = new Predicate() + { + @Override + public boolean check (Glyph glyph) + { + return glyph.isManualShape(); + } + }; + + /** Predicate to check for a barline shape */ + public static final Predicate barPredicate = new Predicate() + { + @Override + public boolean check (Glyph glyph) + { + return glyph.isBar(); + } + }; + + /** A immutable empty set of glyphs */ + public static final Set NO_GLYPHS = Collections.emptySet(); + + //~ Methods ---------------------------------------------------------------- + //----------// + // contains // + //----------// + /** + * Check whether a collection of glyphs contains at least one glyph + * for which the provided predicate holds true. + * + * @param glyphs the glyph collection to check + * @param predicate the predicate to be used + * @return true if there is at least one matching glyph + */ + public static boolean contains (Collection glyphs, + Predicate predicate) + { + return firstOf(glyphs, predicate) != null; + } + + //-----------------// + // containsBarline // + //-----------------// + /** + * Check whether the collection of glyphs contains at least one + * barline. + * + * @param glyphs the collection to check + * @return true if one or several glyphs are barlines components + */ + public static boolean containsBarline (Collection glyphs) + { + return firstOf(glyphs, barPredicate) != null; + } + + //------------// + // containsId // + //------------// + public static boolean containsId (Collection glyphs, + int id) + { + for (Glyph glyph : glyphs) { + if (glyph.getId() == id) { + return true; + } + } + + return false; + } + + //----------------// + // containsManual // + //----------------// + /** + * Check whether a collection of glyphs contains at least one glyph + * with a manually assigned shape. + * + * @param glyphs the glyph collection to check + * @return true if there is at least one manually assigned shape + */ + public static boolean containsManual (Collection glyphs) + { + return contains(glyphs, manualPredicate); + } + + //---------// + // firstOf // + //---------// + /** + * Report the first glyph, if any, for which the provided predicate + * holds true. + * + * @param glyphs the glyph collection to check + * @param predicate the glyph predicate + * @return the first matching glyph found if any, null otherwise + */ + public static Glyph firstOf (Collection glyphs, + Predicate predicate) + { + for (Glyph glyph : glyphs) { + if (predicate.check(glyph)) { + return glyph; + } + } + + return null; + } + + //-----------// + // getBounds // + //-----------// + /** + * Return the display bounding box of a collection of glyphs. + * + * @param glyphs the provided collection of glyphs + * @return the bounding contour + */ + public static Rectangle getBounds (Collection glyphs) + { + Rectangle box = null; + + for (Glyph glyph : glyphs) { + if (box == null) { + box = new Rectangle(glyph.getBounds()); + } else { + box.add(glyph.getBounds()); + } + } + + return box; + } + + //----------------------------// + // getReverseLengthComparator // + //----------------------------// + /** + * For comparing glyph instances on decreasing length. + * + * @param orientation the desired orientation reference + * @return the comparator + */ + public static Comparator getReverseLengthComparator ( + final Orientation orientation) + { + return new Comparator() + { + @Override + public int compare (Glyph s1, + Glyph s2) + { + return s2.getLength(orientation) + - s1.getLength(orientation); + } + }; + } + + //----------------// + // getThicknessAt // + //----------------// + /** + * Report the resulting thickness of the collection of sticks at + * the provided coordinate. + * + * @param coord the desired coordinate + * @param orientation the desired orientation reference + * @param glyphs glyphs contributing to the resulting thickness + * @return the thickness measured, expressed in number of pixels. + */ + public static double getThicknessAt (double coord, + Orientation orientation, + Glyph... glyphs) + { + return getThicknessAt(coord, orientation, null, glyphs); + } + + //----------------// + // getThicknessAt // + //----------------// + /** + * Report the resulting thickness of the collection of sticks at + * the provided coordinate. + * + * @param coord the desired coordinate + * @param orientation the desired orientation reference + * @param section section contributing to the resulting thickness + * @param glyphs glyphs contributing to the resulting thickness + * @return the thickness measured, expressed in number of pixels. + */ + public static double getThicknessAt (double coord, + Orientation orientation, + Section section, + Glyph... glyphs) + { + if (glyphs.length == 0) { + if (section == null) { + return 0; + } else { + return section.getMeanThickness(orientation); + } + } + + // Retrieve global bounds + Rectangle absBox = null; + + if (section != null) { + absBox = section.getBounds(); + } + + for (Glyph g : glyphs) { + if (absBox == null) { + absBox = g.getBounds(); + } else { + absBox.add(g.getBounds()); + } + } + + Rectangle oBox = orientation.oriented(absBox); + int intCoord = (int) Math.floor(coord); + + if ((intCoord < oBox.x) || (intCoord >= (oBox.x + oBox.width))) { + return 0; + } + + // Use a large-enough collector + final Rectangle oRoi = new Rectangle(intCoord, oBox.y, 0, oBox.height); + final Scale scale = new Scale(glyphs[0].getInterline()); + final int probeHalfWidth = scale.toPixels( + BasicAlignment.getProbeWidth()) / 2; + oRoi.grow(probeHalfWidth, 0); + + PointsCollector collector = new PointsCollector( + orientation.absolute(oRoi)); + + // Collect sections contribution + for (Glyph g : glyphs) { + for (Section sct : g.getMembers()) { + sct.cumulate(collector); + } + } + + // Contributing section, if any + if (section != null) { + section.cumulate(collector); + } + + // Case of no pixels found + if (collector.getSize() == 0) { + return 0; + } + + // Analyze range of Y values + int minVal = Integer.MAX_VALUE; + int maxVal = Integer.MIN_VALUE; + int[] vals = (orientation == Orientation.HORIZONTAL) + ? collector.getYValues() : collector.getXValues(); + + for (int i = 0, iBreak = collector.getSize(); i < iBreak; i++) { + int val = vals[i]; + minVal = Math.min(minVal, val); + maxVal = Math.max(maxVal, val); + } + + return maxVal - minVal + 1; + } + + //----------// + // glyphsOf // + //----------// + /** + * Report the set of glyphs that are pointed back by the provided + * collection of sections. + * + * @param sections the provided sections + * @return the set of active containing glyphs + */ + public static Set glyphsOf (Collection
sections) + { + Set glyphs = new LinkedHashSet<>(); + + for (Section section : sections) { + Glyph glyph = section.getGlyph(); + + if (glyph != null) { + glyphs.add(glyph); + } + } + + return glyphs; + } + + //--------------// + // lookupGlyphs // + //--------------// + /** + * Look up in a collection of glyphs for all glyphs + * contained in a provided rectangle. + * + * @param collection the collection of glyphs to be browsed + * @param rect the coordinates rectangle + * @return the glyphs found, which may be an empty list + */ + public static Set lookupGlyphs ( + Collection collection, + Rectangle rect) + { + Set set = new LinkedHashSet<>(); + + for (Glyph glyph : collection) { + if (rect.contains(glyph.getBounds())) { + set.add(glyph); + } + } + + return set; + } + + //--------------// + // lookupGlyphs // + //--------------// + /** + * Look up in a collection of glyphs for all glyphs + * contained in a provided polygon. + * + * @param collection the collection of glyphs to be browsed + * @param polygon the containing polygon + * @return the glyphs found, which may be an empty list + */ + public static Set lookupGlyphs ( + Collection collection, + Polygon polygon) + { + Set set = new LinkedHashSet<>(); + + for (Glyph glyph : collection) { + if (polygon.contains(glyph.getBounds())) { + set.add(glyph); + } + } + + return set; + } + + //--------------// + // lookupGlyphs // + //--------------// + /** + * Look up in a collection of glyphs for all glyphs + * compatible with a provided predicate. + * + * @param collection the collection of glyphs to be browsed + * @param predicate the predicate to apply to each candidate (a null + * predicate will accept all candidates) + * @return the glyphs found, which may be an empty list + */ + public static Set lookupGlyphs ( + Collection collection, + Predicate predicate) + { + Set set = new LinkedHashSet<>(); + + for (Glyph glyph : collection) { + if ((predicate == null) || predicate.check(glyph)) { + set.add(glyph); + } + } + + return set; + } + + //-------------------------// + // lookupIntersectedGlyphs // + //-------------------------// + /** + * Look up in a collection of glyphs for all glyphs + * intersected by a provided rectangle. + * + * @param collection the collection of glyphs to be browsed + * @param rect the coordinates rectangle + * @return the glyphs found, which may be an empty list + */ + public static Set lookupIntersectedGlyphs ( + Collection collection, + Rectangle rect) + { + Set set = new LinkedHashSet<>(); + + for (Glyph glyph : collection) { + if (rect.intersects(glyph.getBounds())) { + set.add(glyph); + } + } + + return set; + } + + //-------// + // purge // + //-------// + /** + * Purge a collection of glyphs of those which match the given + * predicate. + * + * @param glyphs the glyph collection to purge + * @param predicate the predicate to detect glyphs to purge + */ + public static void purge (Collection glyphs, + Predicate predicate) + { + if (predicate == null) { + return; + } + + for (Iterator it = glyphs.iterator(); it.hasNext();) { + Glyph glyph = it.next(); + + if (predicate.check(glyph)) { + it.remove(); + } + } + } + + //--------------// + // purgeManuals // + //--------------// + /** + * Purge a collection of glyphs of those which exhibit a manually + * assigned shape. + * + * @param glyphs the glyph collection to purge + */ + public static void purgeManuals (Collection glyphs) + { + for (Iterator it = glyphs.iterator(); it.hasNext();) { + Glyph glyph = it.next(); + + if (glyph.isManualShape()) { + it.remove(); + } + } + } + + //------------// + // sectionsOf // + //------------// + /** + * Report the set of sections contained by the provided collection + * of glyphs. + * + * @param glyphs the provided glyphs + * @return the set of all member sections + */ + public static Set
sectionsOf (Collection glyphs) + { + Set
sections = new TreeSet<>(); + + for (Glyph glyph : glyphs) { + sections.addAll(glyph.getMembers()); + } + + return sections; + } + + //----------// + // shapesOf // + //----------// + /** + * Report the set of shapes that appear in at least one of the + * provided glyphs. + * + * @param glyphs the provided collection of glyphs + * @return the shapes assigned among these glyphs + */ + public static Set shapesOf (Collection glyphs) + { + EnumSet shapes = EnumSet.noneOf(Shape.class); + + if (glyphs != null) { + for (Glyph glyph : glyphs) { + if (glyph.getShape() != null) { + shapes.add(glyph.getShape()); + } + } + } + + return shapes; + } + + //-----------// + // sortedSet // + //-----------// + /** + * Build a mutable set with the provided glyphs. + * + * @param glyphs the provided glyphs + * @return a mutable sorted set composed of these glyphs + */ + public static SortedSet sortedSet (Glyph... glyphs) + { + SortedSet set = new TreeSet<>(Glyph.byAbscissa); + + if (glyphs.length > 0) { + set.addAll(Arrays.asList(glyphs)); + } + + return set; + } + + //-----------// + // sortedSet // + //-----------// + /** + * Build a mutable set with the provided glyphs. + * + * @param glyphs the provided glyphs + * @return a mutable sorted set composed of these glyphs + */ + public static SortedSet sortedSet (Collection glyphs) + { + SortedSet set = new TreeSet<>(Glyph.byAbscissa); + set.addAll(glyphs); + + return set; + } + + //----------// + // toString // + //----------// + /** + * Build a string with just the ids of the glyph collection, + * introduced by the provided label. + * + * @param label the string that introduces the list of IDs + * @param glyphs the collection of glyphs + * @return the string built + */ + public static String toString (String label, + Collection glyphs) + { + if (glyphs == null) { + return ""; + } + + StringBuilder sb = new StringBuilder(); + sb.append(label) + .append("["); + + for (Glyph glyph : glyphs) { + sb.append("#") + .append(glyph.getId()); + } + + sb.append("]"); + + return sb.toString(); + } + + //----------// + // toString // + //----------// + /** + * Build a string with just the ids of the glyph array, introduced + * by the provided label. + * + * @param label the string that introduces the list of IDs + * @param glyphs the array of glyphs + * @return the string built + */ + public static String toString (String label, + Glyph... glyphs) + { + return toString(label, Arrays.asList(glyphs)); + } + + //----------// + // toString // + //----------// + /** + * Build a string with just the ids of the glyph collection, + * introduced by the label "glyphs". + * + * @param glyphs the collection of glyphs + * @return the string built + */ + public static String toString (Collection glyphs) + { + return toString("glyphs", glyphs); + } + + //----------// + // toString // + //----------// + /** + * Build a string with just the ids of the glyph array, introduced + * by the label "glyphs". + * + * @param glyphs the array of glyphs + * @return the string built + */ + public static String toString (Glyph... glyphs) + { + return toString("glyphs", glyphs); + } + + private Glyphs () + { + } +} diff --git a/src/main/omr/glyph/GlyphsBuilder.java b/src/main/omr/glyph/GlyphsBuilder.java new file mode 100644 index 0000000..99c4b1e --- /dev/null +++ b/src/main/omr/glyph/GlyphsBuilder.java @@ -0,0 +1,583 @@ +//----------------------------------------------------------------------------// +// // +// G l y p h s B u i l d e r // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +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). + * + *

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). + * + *

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). + * + *

It provides provisioning methods to actually insert or remove a glyph: + *

    + * + *
  • A given newly built glyph can be inserted via {@link #addGlyph}
  • + * + *
  • Similarly {@link #removeGlyph} allows the removal of an existing glyph. + * Nota: 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.
  • + *
+ * + * @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
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 retrieveGlyphs (List
sections, + Nest nest, + Scale scale) + { + List 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
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 parts) + { + // Gather all the sections involved + Collection
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
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
allSections = new ArrayList<>(); + allSections.addAll(system.getVerticalSections()); + allSections.addAll(system.getHorizontalSections()); + + List 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 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 glyphs, + Glyph glyph) + { + if (glyph.isStem()) { + return null; + } + + // Box for stem(s) lookup + final Rectangle box = stemBoxOf(glyph, side); + final List 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() + { + @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"); + + } +} diff --git a/src/main/omr/glyph/GlyphsModel.java b/src/main/omr/glyph/GlyphsModel.java new file mode 100644 index 0000000..7ea4dd5 --- /dev/null +++ b/src/main/omr/glyph/GlyphsModel.java @@ -0,0 +1,370 @@ +//----------------------------------------------------------------------------// +// // +// G l y p h s M o d e l // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.glyph; + +import omr.Main; + +import omr.glyph.facets.BasicGlyph; +import omr.glyph.facets.Glyph; + +import omr.grid.StaffInfo; + +import omr.lag.Section; + +import omr.score.ui.ScoreActions; + +import omr.sheet.Sheet; +import omr.sheet.SystemInfo; + +import omr.step.Step; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.Collection; + +/** + * Class {@code GlyphsModel} is a common model for synchronous glyph + * and section handling. + * + *

Nota: User gesture should trigger actions in GlyphsController which will + * asynchronously delegate to this model. + * + * @author Hervé Bitteur + */ +public class GlyphsModel +{ + //~ Static fields/initializers --------------------------------------------- + + /** Usual logger utility */ + private static final Logger logger = LoggerFactory.getLogger(GlyphsModel.class); + + //~ Instance fields -------------------------------------------------------- + /** Underlying glyph nest */ + protected final Nest nest; + + /** Related Sheet */ + protected final Sheet sheet; + + /** Related Step */ + protected final Step step; + + /** Latest shape assigned if any */ + protected Shape latestShape; + + //~ Constructors ----------------------------------------------------------- + //-------------// + // GlyphsModel // + //-------------// + /** + * Create an instance of GlyphsModel, with its underlying glyph lag. + * + * @param sheet the related sheet (can be null) + * @param nest the related nest (cannot be null) + * @param step the step after which update should be perform (can be null) + */ + public GlyphsModel (Sheet sheet, + Nest nest, + Step step) + { + // Null sheet is allowed (for GlyphVerifier use) + this.sheet = sheet; + + if (nest == null) { + throw new IllegalArgumentException( + "Attempt to create a GlyphsModel with null underlying nest"); + } else { + this.nest = nest; + } + + this.step = step; + } + + //~ Methods ---------------------------------------------------------------- + //--------------// + // assignGlyphs // + //--------------// + /** + * Assign a shape to the selected collection of glyphs. + * + * @param glyphs the collection of glyphs to be assigned + * @param shape the shape to be assigned + * @param compound flag to build one compound, rather than assign each + * individual glyph + * @param grade the grade we have wrt the assigned shape + */ + public void assignGlyphs (Collection glyphs, + Shape shape, + boolean compound, + double grade) + { + if (compound) { + // Build & insert one compound + Glyph glyph; + + SystemInfo system = sheet.getSystemOf(glyphs); + + if (system != null) { + glyph = system.buildTransientCompound(glyphs); + } else { + glyph = new BasicGlyph(sheet.getScale().getInterline()); + + for (Glyph g : glyphs) { + glyph.stealSections(g); + + if (glyph.getNest() == null) { + glyph.setNest(g.getNest()); + } + } + } + + assignGlyph(glyph, shape, grade); + } else { + // Assign each glyph individually + for (Glyph glyph : new ArrayList<>(glyphs)) { + if (glyph.getShape() != Shape.NOISE) { + assignGlyph(glyph, shape, grade); + } + } + } + } + + //----------------// + // assignSections // + //----------------// + /** + * Assign a shape to the selected collection of sections. + * + * @param sections the collection of sections to be aggregated as a glyph + * @param shape the shape to be assigned + * @param grade the grade we have wrt the assigned shape + * @return the newly built glyph + */ + public Glyph assignSections (Collection

sections, + Shape shape, + double grade) + { + // Build & insert one glyph out of the sections + SystemInfo system = sections.iterator().next().getSystem(); + Glyph glyph = system.buildGlyph(sections); + + return assignGlyph(glyph, shape, grade); + } + + //----------------// + // deassignGlyphs // + //----------------// + /** + * De-Assign a collection of glyphs. + * + * @param glyphs the collection of glyphs to be de-assigned + */ + public void deassignGlyphs (Collection glyphs) + { + for (Glyph glyph : new ArrayList<>(glyphs)) { + deassignGlyph(glyph); + } + } + + //--------------// + // deleteGlyphs // + //--------------// + public void deleteGlyphs (Collection glyphs) + { + for (Glyph glyph : new ArrayList<>(glyphs)) { + deleteGlyph(glyph); + } + } + + //--------------// + // getGlyphById // + //--------------// + /** + * Retrieve a glyph, knowing its id. + * + * @param id the glyph id + * @return the glyph found, or null if not + */ + public Glyph getGlyphById (int id) + { + return nest.getGlyph(id); + } + + //----------------// + // getLatestShape // + //----------------// + /** + * Report the latest non null shape that was assigned, or null if + * none. + * + * @return latest shape assigned, or null if none + */ + public Shape getLatestShape () + { + return latestShape; + } + + //---------// + // getNest // + //---------// + /** + * Report the underlying glyph nest. + * + * @return the related glyph nest + */ + public Nest getNest () + { + return nest; + } + + //----------------// + // getRelatedStep // + //----------------// + /** + * Report the step this GlyphsModel is used for, so that we know + * from which step updates must be propagated. + * (we have to update the steps that follow this one) + * + * @return the step related to this glyphs model + */ + public Step getRelatedStep () + { + return step; + } + + //----------// + // getSheet // + //----------// + /** + * Report the model underlying sheet. + * + * @return the underlying sheet instance + */ + public Sheet getSheet () + { + return sheet; + } + + //----------------// + // setLatestShape // + //----------------// + /** + * Assign the latest useful shape. + * + * @param shape the current / latest shape + */ + public void setLatestShape (Shape shape) + { + if (shape != Shape.GLYPH_PART) { + latestShape = shape; + } + } + + //-------------// + // assignGlyph // + //-------------// + /** + * Assign a Shape to a glyph, inserting the glyph to its containing + * system and nest if it is still transient. + * + * @param glyph the glyph to be assigned + * @param shape the assigned shape, which may be null + * @param grade the grade about shape + * @return the assigned glyph (perhaps an original glyph) + */ + protected Glyph assignGlyph (Glyph glyph, + Shape shape, + double grade) + { + if (glyph == null) { + return null; + } + + if (shape != null) { + SystemInfo system = sheet.getSystemOf(glyph); + + if (system != null) { + glyph = system.addGlyph(glyph); // System then nest + } else { + // Insert in nest directly, which assigns an id to the glyph + glyph = nest.addGlyph(glyph); + } + + boolean isTransient = glyph.isTransient(); + logger.debug("Assign {}{} to {}", + isTransient ? "compound " : "", glyph.idString(), shape); + + // Remember the latest shape assigned + setLatestShape(shape); + } + + // Do the assignment of the shape to the glyph + glyph.setShape(shape, grade); + + // Should we persist the assigned glyph? + if ((shape != null) + && (grade == Evaluation.MANUAL) + && (Main.getGui() != null) + && ScoreActions.getInstance().isManualPersisted()) { + // Record the glyph description to disk + GlyphRepository.getInstance().recordOneGlyph(glyph, sheet); + } + + return glyph; + } + + //---------------// + // deassignGlyph // + //---------------// + /** + * Deassign the shape of a glyph. + * + * @param glyph the glyph to deassign + */ + protected void deassignGlyph (Glyph glyph) + { + // Assign the null shape to the glyph + assignGlyph(glyph, null, Evaluation.ALGORITHM); + } + + //-------------// + // deleteGlyph // + //-------------// + protected void deleteGlyph (Glyph glyph) + { + if (glyph == null) { + return; + } + + if (!glyph.isVirtual()) { + logger.warn("Attempt to delete non-virtual {}", glyph.idString()); + + return; + } + + + SystemInfo system = sheet.getSystemOf(glyph); + + // Special case for ledger glyph + if (glyph.getShape() == Shape.LEDGER) { + StaffInfo staff = system.getStaffAt(glyph.getAreaCenter()); + staff.removeLedger(glyph); + } + + if (system != null) { + system.removeGlyph(glyph); + } + + nest.removeVirtualGlyph((VirtualGlyph) glyph); + } +} diff --git a/src/main/omr/glyph/Grades.java b/src/main/omr/glyph/Grades.java new file mode 100644 index 0000000..32c1361 --- /dev/null +++ b/src/main/omr/glyph/Grades.java @@ -0,0 +1,180 @@ +//----------------------------------------------------------------------------// +// // +// G r a d e s // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.glyph; + +import omr.constant.ConstantSet; + +/** + * Class {@code Grades} gathers in one class all the various + * evaluation grades used throughout the application. + * + * @author Hervé Bitteur + */ +public class Grades +{ + //~ Static fields/initializers --------------------------------------------- + + /** Specific application parameters */ + private static final Constants constants = new Constants(); + + // Maximum values + //--------------- + /** Maximum grade for a glyph before being merged in a compound */ + public static final double compoundPartMaxGrade = constants.compoundPartMaxGrade.getValue(); + + // Minimum values + //--------------- + /** Minimum grade for a bass clef glyph */ + public static final double bassMinGrade = constants.bassMinGrade.getValue(); + + /** Minimum grade for a bass clef glyph */ + public static final double clefMinGrade = constants.clefMinGrade.getValue(); + + /** Minimum grade for a consistent note */ + public static final double consistentNoteMinGrade = constants.consistentNoteMinGrade.getValue(); + + /** Minimum grade for a hook glyph */ + public static final double forteMinGrade = constants.forteMinGrade.getValue(); + + /** Minimum grade for a hook glyph */ + public static final double hookMinGrade = constants.hookMinGrade.getValue(); + + /** Minimum grade for a key signature */ + public static final double keySigMinGrade = constants.keySigMinGrade.getValue(); + + /** Minimum grade for a glyph left over */ + public static final double leftOverMinGrade = constants.leftOverMinGrade.getValue(); + + /** Minimum grade for a leaf glyph */ + public static final double ledgerNoteMinGrade = constants.ledgerNoteMinGrade.getValue(); + + /** Minimum grade for a merged note */ + public static final double mergedNoteMinGrade = constants.mergedNoteMinGrade.getValue(); + + /** Minimum grade for a part of split glyph */ + public static final double partMinGrade = constants.partMinGrade.getValue(); + + /** Minimum grade for a patterns-issued glyph */ + public static final double patternsMinGrade = constants.patternsMinGrade.getValue(); + + /** Minimum grade for a symbol glyph */ + public static final double symbolMinGrade = constants.symbolMinGrade.getValue(); + + /** Minimum grade for a text glyph */ + public static final double textMinGrade = constants.textMinGrade.getValue(); + + /** Minimum grade for a time glyph */ + public static final double timeMinGrade = constants.timeMinGrade.getValue(); + + /** Minimum grade for a validation */ + public static final double validationMinGrade = constants.validationMinGrade.getValue(); + + /** No minimum grade */ + public static final double noMinGrade = 0; + + //~ Constructors ----------------------------------------------------------- + private Grades () + { + } + + //~ Inner Classes ---------------------------------------------------------- + //-----------// + // Constants // + //-----------// + private static final class Constants + extends ConstantSet + { + //~ Instance fields ---------------------------------------------------- + + Evaluation.Grade compoundPartMaxGrade = new Evaluation.Grade( + 90, + "*MAXIMUM* grade for a suitable compound part"); + + //---------------------------------------------------------------------- + // Minimum values (please keep them sorted by decreasing value) + // + Evaluation.Grade validationMinGrade = new Evaluation.Grade( + 83, + "Minimum grade for a validation"); + + // + Evaluation.Grade symbolMinGrade = new Evaluation.Grade( + 77, + "Minimum grade for a symbol"); + + // + Evaluation.Grade patternsMinGrade = new Evaluation.Grade( + 67, + "Minimum grade for pattern phase"); + + // + Evaluation.Grade partMinGrade = new Evaluation.Grade( + 35, + "Minimum grade for a part of a split glyph"); + + // + Evaluation.Grade bassMinGrade = new Evaluation.Grade( + 33, + "Minimum grade for a bass clef"); + + // + Evaluation.Grade hookMinGrade = new Evaluation.Grade( + 20, + "Minimum grade for beam hook verification"); + + // + Evaluation.Grade mergedNoteMinGrade = new Evaluation.Grade( + 20, + "Minimum grade for a merged note"); + + // + Evaluation.Grade leftOverMinGrade = new Evaluation.Grade( + 10, + "Minimum grade for a glyph left over"); + + // + Evaluation.Grade ledgerNoteMinGrade = new Evaluation.Grade( + 10, + "Minimum grade for a ledger note"); + + // + Evaluation.Grade clefMinGrade = new Evaluation.Grade( + 0.3, + "Minimum grade for a clef"); + + // + Evaluation.Grade consistentNoteMinGrade = new Evaluation.Grade( + 0.1, + "Minimum grade for a consistent note head"); + + // + Evaluation.Grade forteMinGrade = new Evaluation.Grade( + 0.01, + "Minimum grade for glyph close to Forte"); + + // + Evaluation.Grade keySigMinGrade = new Evaluation.Grade( + 0.01, + "Minimum grade for a key signature"); + + // + Evaluation.Grade textMinGrade = new Evaluation.Grade( + 0.01, + "Minimum grade for a text symbol"); + + // + Evaluation.Grade timeMinGrade = new Evaluation.Grade( + 0, + "Minimum grade for a time sig"); + + } +} diff --git a/src/main/omr/glyph/Nest.java b/src/main/omr/glyph/Nest.java new file mode 100644 index 0000000..3234b09 --- /dev/null +++ b/src/main/omr/glyph/Nest.java @@ -0,0 +1,235 @@ +//----------------------------------------------------------------------------// +// // +// N e s t // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.glyph; + +import omr.glyph.facets.Glyph; + +import omr.lag.Section; + +import omr.math.Histogram; + +import omr.run.Orientation; + +import omr.selection.GlyphEvent; +import omr.selection.GlyphIdEvent; +import omr.selection.GlyphSetEvent; +import omr.selection.SelectionService; + +import java.awt.Point; +import java.awt.Rectangle; +import java.util.Collection; +import java.util.Set; + +/** + * Class {@code Nest} handles a collection of {@link Glyph} instances, + * with the ability to retrieve a Glyph based on its Id or its location, + * and the ability to give birth to new glyphs. + * + *

A nest has no orientation, nor any of its glyphs since a glyph is a + * collection of sections that can be differently oriented.

+ * + *

A glyph is made of member sections and always keeps a collection of its + * member sections. Sections are made of runs of pixels and thus sections do not + * overlap. Different glyphs can have sections in common, and in that case they + * overlap, however only one of these glyphs is the current "owner" of these + * common sections. It is known as being "active" while the others are inactive. + *

+ * + *

A nest hosts a SelectionService that deals with glyph selection + * (Events related to Glyph, GlyphId and GlyphSet). + * + *

Selecting a (foreground) pixel, thus selects its containing section, and + * its active glyph if any.

+ * + * @author Hervé Bitteur + */ +public interface Nest +{ + //~ Static fields/initializers --------------------------------------------- + + /** Events that can be published on a nest service */ + static final Class[] eventsWritten = new Class[]{ + GlyphEvent.class, + GlyphIdEvent.class, + GlyphSetEvent.class + }; + + //~ Methods ---------------------------------------------------------------- + /** + * Register a glyph and make sure all its member sections point back + * to it. + * + * @param glyph the glyph to add to the nest + * @return the actual glyph (already existing or brand new) + */ + Glyph addGlyph (Glyph glyph); + + /** + * Remove link and subscription to locationService + * + * @param locationService thte location service + */ + void cutServices (SelectionService locationService); + + /** + * Print out major internal info about this glyph nest. + * + * @param title a specific title to be used for the dumpOf + */ + String dumpOf (String title); + + /** + * Export the unmodifiable collection of active glyphs of the nest. + * + * @return the collection of glyphs for which at least a section is assigned + */ + Collection getActiveGlyphs (); + + /** + * Export the whole unmodifiable collection of glyphs of the nest. + * + * @return the collection of glyphs, both active and inactive + */ + Collection getAllGlyphs (); + + /** + * Retrieve a glyph via its Id among the collection of glyphs + * + * @param id the glyph id to search for + * @return the glyph found, or null otherwise + */ + Glyph getGlyph (Integer id); + + /** + * Report the nest selection service + * + * @return the nest selection service (Glyph, GlyphSet, GlyphId) + */ + SelectionService getGlyphService (); + + /** + * Get the pixel histogram for a collection of glyphs, in the + * specified orientation. + * + * @param orientation specific orientation desired for the histogram + * @param glyphs the provided collection of glyphs + * @return the histogram of projected pixels + */ + Histogram getHistogram (Orientation orientation, + Collection glyphs); + + /** + * Report a name for this nest instance + * + * @return a (distinguished) name + */ + String getName (); + + /** + * Return the original glyph, if any, that the provided glyph + * duplicates. + * + * @param glyph the provided glyph + * @return the original for this glyph, if any, otherwise null + */ + Glyph getOriginal (Glyph glyph); + + /** + * Return the original glyph, if any, that corresponds to the + * provided signature. + * + * @param signature the provided signature + * @return the original glyph for this signature, if any, otherwise null + */ + Glyph getOriginal (GlyphSignature signature); + + /** + * Report the glyph currently selected, if any + * + * @return the current glyph, or null + */ + Glyph getSelectedGlyph (); + + /** + * Report the glyph set currently selected, if any + * + * @return the current glyph set, or null + */ + Set getSelectedGlyphSet (); + + /** + * Check whether the provided glyph is among the VIP ones + * + * @param glyph the glyph (ID) to check + * @return true if this is a vip glyph + */ + boolean isVip (Glyph glyph); + + /** + * Look up for all active glyphs contained in a provided + * rectangle. + * + * @param rect the coordinates rectangle + * @return the glyphs found, which may be an empty list + */ + Set lookupGlyphs (Rectangle rect); + + /** + * Look up for all active glyphs intersected by a provided + * rectangle. + * + * @param rect the coordinates rectangle + * @return the glyphs found, which may be an empty list + */ + Set lookupIntersectedGlyphs (Rectangle rect); + + /** + * Look for a virtual glyph whose box contains the designated point + * + * @param point the designated point + * @return the virtual glyph found, or null + */ + Glyph lookupVirtualGlyph (Point point); + + /** + * Map a section to a glyph, making the glyph active + * + * @param section the section to map + * @param glyph the assigned glyph + */ + void mapSection (Section section, + Glyph glyph); + + /** + * Simply register a glyph in the graph, making sure we do not + * duplicate any existing glyph. + * (a glyph being really defined by the set of its member sections) + * + * @param glyph the glyph to add to the nest + * @return the actual glyph (already existing or brand new) + */ + Glyph registerGlyph (Glyph glyph); + + /** + * Remove the provided virtual glyph + * + * @param glyph the virtual glyph to remove + */ + void removeVirtualGlyph (VirtualGlyph glyph); + + /** + * Inject dependency on location service, and trigger subscriptions + * + * @param locationService the location service + */ + void setServices (SelectionService locationService); +} diff --git a/src/main/omr/glyph/SectionSets.java b/src/main/omr/glyph/SectionSets.java new file mode 100644 index 0000000..95673ed --- /dev/null +++ b/src/main/omr/glyph/SectionSets.java @@ -0,0 +1,285 @@ +//----------------------------------------------------------------------------// +// // +// S e c t i o n S e t s // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.glyph; + +import omr.glyph.facets.Glyph; + +import omr.lag.Lag; +import omr.lag.Section; +import omr.lag.Sections; + +import omr.run.Orientation; + +import omr.sheet.Sheet; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import javax.xml.bind.Marshaller; +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlAttribute; +import javax.xml.bind.annotation.XmlElement; + +/** + * Class {@code SectionSets} handles a collection of section sets, + * with the ability to (un)marshall its content using the sections ids. + * + * @author Hervé Bitteur + */ +@XmlAccessorType(XmlAccessType.NONE) +public class SectionSets +{ + //~ Static fields/initializers --------------------------------------------- + + /** Usual logger utility */ + private static final Logger logger = LoggerFactory.getLogger(SectionSets.class); + + //~ Instance fields -------------------------------------------------------- + /** The collection of sections sets */ + protected Collection> sets; + + /** The collection of sets (=glyphs) of section descriptors */ + @XmlElement(name = "sections") + private Collection descSets; + + //~ Constructors ----------------------------------------------------------- + //-------------// + // SectionSets // + //-------------// + /** + * Creates a new SectionSets object. + * + * @param sets The collection of collections of sections + */ + public SectionSets (Collection> sets) + { + this.sets = sets; + } + + //-------------// + // SectionSets // No-arg constructor needed by JAXB + //-------------// + private SectionSets () + { + } + + //~ Methods ---------------------------------------------------------------- + //------------------// + // createFromGlyphs // + //------------------// + /** + * Convenient method to create the proper SectionSets out of a provided + * collection of glyphs + * + * @param glyphs the provided glyphs + * @return a newly built SectionSets instance + */ + public static SectionSets createFromGlyphs (Collection glyphs) + { + SectionSets sectionSets = new SectionSets(); + sectionSets.sets = new ArrayList<>(); + + for (Glyph glyph : glyphs) { + sectionSets.sets.add(new ArrayList<>(glyph.getMembers())); + } + + return sectionSets; + } + + //--------------------// + // createFromSections // + //--------------------// + /** + * Convenient method to create the proper SectionSets out of a provided + * collection of sections + * + * @param sections the provided sections + * @return a newly built SectionSets instance (a singleton actually) + */ + public static SectionSets createFromSections (Collection
sections) + { + SectionSets sectionSets = new SectionSets(); + sectionSets.sets = new ArrayList<>(); + sectionSets.sets.add(sections); + + return sectionSets; + } + + //---------// + // getSets // + //---------// + /** + * Report the collection of section sets + * + * @param sheet the containing sheet (needed to get sections from their id) + * @return the collection of section sets + */ + public Collection> getSets (Sheet sheet) + { + if (sets == null) { + sets = new ArrayList<>(); + + for (SectionDescSet idSet : descSets) { + List
sectionSet = new ArrayList<>(); + + for (SectionDesc sectionId : idSet.sections) { + Lag lag = (sectionId.orientation == Orientation.VERTICAL) + ? sheet.getVerticalLag() + : sheet.getHorizontalLag(); + Section section = lag.getVertexById(sectionId.id); + + if (section == null) { + logger.warn(sheet.getLogPrefix() + + "Cannot find section for " + sectionId, + new Throwable()); + } else { + sectionSet.add(section); + } + } + + sets.add(sectionSet); + } + } + + return sets; + } + + //----------// + // toString // + //----------// + @Override + public String toString () + { + if (sets != null) { + StringBuilder sb = new StringBuilder(); + + for (Collection
set : sets) { + // Separator needed? + if (sb.length() > 0) { + sb.append(" "); + } + + sb.append(Sections.toString(set)); + } + + return sb.toString(); + } + + return ""; + } + + //---------------// + // beforeMarshal // + //---------------// + /** + * Called immediately before the marshalling of this object begins. + */ + @SuppressWarnings("unused") + private void beforeMarshal (Marshaller m) + { + // Convert sections -> ids + if (sets != null) { + descSets = new ArrayList<>(); + + for (Collection
set : sets) { + SectionDescSet descSet = new SectionDescSet(); + + for (Section section : set) { + descSet.sections.add( + new SectionDesc( + section.getId(), + section.getOrientation())); + } + + descSets.add(descSet); + } + } + } + + //~ Inner Classes ---------------------------------------------------------- + //-------------// + // SectionDesc // + //-------------// + /** + * Descriptor for one section + */ + private static class SectionDesc + { + //~ Instance fields ---------------------------------------------------- + + // Annotation to get all ids, space-separated, in one single element: + //@XmlList + // Annotation to avoid any wrapper: + //@XmlValue + //private Collection ids = new ArrayList(); + /** Section id */ + @XmlAttribute(name = "id") + Integer id; + + /** Section orientation */ + @XmlAttribute(name = "orientation") + Orientation orientation; + + //~ Constructors ------------------------------------------------------- + // For JAXB + public SectionDesc () + { + } + + public SectionDesc (Integer id, + Orientation orientation) + { + this.id = id; + this.orientation = orientation; + } + + //~ Methods ------------------------------------------------------------ + @Override + public String toString () + { + StringBuilder sb = new StringBuilder("{SectionDesc"); + sb.append(" ") + .append(orientation); + sb.append(" ") + .append(id); + sb.append("}"); + + return super.toString(); + } + } + + //----------------// + // SectionDescSet // + //----------------// + /** + * Handles one collection of section ids. + * The only purpose of this class (vs the direct use of List) is + * the ability to add annotations meant for JAXB + */ + private static class SectionDescSet + { + //~ Instance fields ---------------------------------------------------- + + // // Annotation to get all ids, space-separated, in one single element: + // @XmlList + // // Annotation to avoid any wrapper: + // @XmlValue + @XmlElement(name = "section") + private Collection sections = new ArrayList<>(); + + } +} diff --git a/src/main/omr/glyph/Shape.java b/src/main/omr/glyph/Shape.java new file mode 100644 index 0000000..ec2eb4d --- /dev/null +++ b/src/main/omr/glyph/Shape.java @@ -0,0 +1,723 @@ +//----------------------------------------------------------------------------// +// // +// S h a p e // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.glyph; + +import omr.constant.Constant; + +import omr.ui.symbol.ShapeSymbol; +import omr.ui.symbol.Symbols; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.awt.Color; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; + +/** + * Class {@code Shape} defines the comprehensive list of glyph shapes. + * It is organized according to the Unicode Standard 4.0, with a few addition + * for convenience only. + * + *

The enumeration begins with physical shapes (which are the + * only ones usable for training) and ends with the logical shapes. + * The method {@link #isTrainable()} can be used to disambiguate between + * physical and logical shapes.

+ * + *

Nota: All the physical shapes MUST have different + * characteristics for the training to work correctly. + * The ART evaluator uses moments that are invariant to translation, scaling + * and rotation (and to symmetry as well). + * Shapes that exhibit some symmetry (like FERMATA vs FERMATA_BELOW) would + * be considered as the same shape by the ART evaluator. + * Therefore, the strategy is to define a single shape (FERMATA_SET) for the + * evaluator, leaving the final disambiguation between FERMATA_BELOW and + * FERMATA to tests performed beyond the ART evaluator. + * FERMATA_SET belongs to the physical shapes, while FERMATA_BELOW and + * FERMATA belong to the logical shapes. + * All shapes whose name ends with "_set" are in this case.

+ * + *

As far as possible, a symbol should be generated for every shape.

+ * + *

A shape may have a related "decorated" symbol. For example the BREVE_REST + * is similar to a black rectangle which is used for training / recognition and + * the related symbol is used for drawing in score view. However, in menu items, + * it is displayed as a black rectangle surrounded by a staff line above and a + * staff line below. + * The method {@link #getDecoratedSymbol()} returns the symbol to use in menu + * items.

+ * + * @author Hervé Bitteur + */ +public enum Shape +{ + + /** + * ================================================================ + * Nota: Avoid changing the order of these physical shapes, + * otherwise the evaluators won't detect this and you'll have to + * retrain them on your own. + * ========================================================================= + */ + // + // Sets -------------------------------------------------------------------- + // + DOT_set("Dot set", new Color(0x0cccc)), + FERMATA_set("Set of Fermata's"), + HW_REST_set("Half & Whole Rest set"), + TIME_69_set("Time 6 & 9 set"), + FLAG_1_set("Single flag set"), + FLAG_2_set("Double flag set"), + FLAG_3_set("Triple flag set"), + FLAG_4_set("Quadruple flag set"), + FLAG_5_set("Quintuple flag set"), + WEDGE_set("Crescendo & Decrescendo set"), + TURN_set("Turn set"), + // + // Bars -------------------------------------------------------------------- + // + DAL_SEGNO("Repeat from the sign"), + DA_CAPO("Repeat from the beginning"), + SEGNO("Sign"), + CODA("Closing section"), + BREATH_MARK("Breath Mark"), + CAESURA("Caesura"), + BRACE("Brace"), + BRACKET("Bracket"), + // + // Clefs ------------------------------------------------------------------- + // + G_CLEF("Treble Clef", new Color(0xff99ff)), + G_CLEF_SMALL("Small Treble Clef", new Color(0xff99ff)), + G_CLEF_8VA("Treble Clef Ottava Alta", new Color(0xff99ff)), + G_CLEF_8VB("Treble Clef Ottava Bassa", new Color(0xff99ff)), + C_CLEF("Ut Clef", new Color(0xff99ff)), + F_CLEF("Bass Clef"), + F_CLEF_SMALL("Small Bass Clef"), + F_CLEF_8VA("Bass Clef Ottava Alta"), + F_CLEF_8VB("Bass Clef Ottava Bassa"), + PERCUSSION_CLEF("Percussion Clef"), + FLAT("Minus one half step", new Color(0x00aaaa)), + NATURAL("Natural value", new Color(0x0066ff)), + SHARP("Plus one half step", new Color(0x3399ff)), + DOUBLE_SHARP("Double Sharp"), + DOUBLE_FLAT("Double Flat"), + TIME_ZERO("Digit 0"), + TIME_ONE("Digit 1"), + TIME_TWO("Digit 2"), + TIME_THREE("Digit 3"), + TIME_FOUR("Digit 4"), + TIME_FIVE("Digit 5"), + TIME_SEVEN("Digit 7"), + TIME_EIGHT("Digit 8"), + TIME_TWELVE("Number 12"), + TIME_SIXTEEN("Number 16"), + TIME_FOUR_FOUR("Rational 4/4"), + TIME_TWO_TWO("Rational 2/2"), + TIME_TWO_FOUR("Rational 2/4"), + TIME_THREE_FOUR("Rational 3/4"), + TIME_SIX_EIGHT("Rational 6/8"), + COMMON_TIME("Alpha = 4/4", new Color(0xcc6600)), + CUT_TIME("Semi-Alpha = 2/2"), + OTTAVA_ALTA("8 va", new Color(0xcc66ff)), + OTTAVA_BASSA("8 vb", new Color(0xcc66ff)), + // + // Key signatures ---------------------------------------------------------- + // + KEY_FLAT_7("Seven Flats"), + KEY_FLAT_6("Six Flats"), + KEY_FLAT_5("Five Flats"), + KEY_FLAT_4("Four Flats"), + KEY_FLAT_3("Three Flats"), + KEY_FLAT_2("Two Flats"), + KEY_SHARP_2("Two Sharps"), + KEY_SHARP_3("Three Sharps"), + KEY_SHARP_4("Four Sharps"), + KEY_SHARP_5("Five Sharps"), + KEY_SHARP_6("Six Sharps"), + KEY_SHARP_7("Seven Sharps"), + // + // Rests ------------------------------------------------------------------- + // + LONG_REST("Rest for 4 measures"), + BREVE_REST("Rest for 2 measures"), + QUARTER_REST("Rest for a 1/4"), + EIGHTH_REST("Rest for a 1/8"), + ONE_16TH_REST("Rest for a 1/16"), + ONE_32ND_REST("Rest for a 1/32"), + ONE_64TH_REST("Rest for a 1/64"), + ONE_128TH_REST("Rest for a 1/128"), + // + // Noteheads --------------------------------------------------------------- + // + NOTEHEAD_VOID("Hollow node head for halves", new Color(0xffcc00)), + NOTEHEAD_VOID_2("Pack of two hollow node heads for halves", new Color(0xffcc00)), + NOTEHEAD_VOID_3("Pack of three hollow node heads for halves", new Color(0xffcc00)), + NOTEHEAD_BLACK("Filled node head for quarters and less"), + NOTEHEAD_BLACK_2("Pack of two filled node heads for quarters and less"), + NOTEHEAD_BLACK_3("Pack of three filled node heads for quarters and less"), + // + // Notes ------------------------------------------------------------------- + // + BREVE("Double Whole"), + WHOLE_NOTE("Hollow node head for wholes"), + WHOLE_NOTE_2("Pack of two hollow node heads for wholes"), + WHOLE_NOTE_3("Pack of three hollow node heads for wholes"), + // + // Beams and slurs --------------------------------------------------------- + // + BEAM("Beam between two stems"), + BEAM_2("Pack of 2 beams"), + BEAM_3("Pack of 3 beams"), + BEAM_HOOK("Hook of a beam attached on one stem"), + SLUR("Slur tying notes", new Color(0xbb8888)), + // + // Articulation ------------------------------------------------------------ + // + ACCENT, + TENUTO, + STACCATISSIMO, + STRONG_ACCENT("Marcato"), + ARPEGGIATO, + // + // Dynamics ---------------------------------------------------------------- + // + DYNAMICS_CHAR_M("m character"), + DYNAMICS_CHAR_R("r character"), + DYNAMICS_CHAR_S("c character"), + DYNAMICS_CHAR_Z("z character"), + DYNAMICS_F("Forte"), + DYNAMICS_FF("Fortissimo"), + DYNAMICS_FFF("Fortississimo"), + DYNAMICS_FP("FortePiano"), + DYNAMICS_FZ("Forzando"), + DYNAMICS_MF("Mezzo forte"), + DYNAMICS_MP("Mezzo piano"), + DYNAMICS_P("Piano"), + DYNAMICS_PP("Pianissimo"), + DYNAMICS_PPP("Pianississimo"), + DYNAMICS_RF, + DYNAMICS_RFZ("Rinforzando"), + DYNAMICS_SF, + DYNAMICS_SFFZ, + DYNAMICS_SFP("Subito fortepiano"), + DYNAMICS_SFPP, + DYNAMICS_SFZ("Sforzando"), + // + // Ornaments --------------------------------------------------------------- + // + GRACE_NOTE_SLASH("Grace Note with a Slash"), + GRACE_NOTE("Grace Note"), + TR("Trill"), + TURN_SLASH("Turn with a Slash"), + MORDENT("Mordent"), + INVERTED_MORDENT("Mordent with a Slash"), + // + // Tuplets ----------------------------------------------------------------- + // + TUPLET_THREE("3", new Color(0xcc00cc)), + TUPLET_SIX("6", new Color(0xcc00cc)), + PEDAL_MARK("Pedal down", new Color(0x009999)), + PEDAL_UP_MARK("Pedal downup", new Color(0x009999)), + // + // Miscellaneous ----------------------------------------------------------- + // + CLUTTER("Pure clutter", new Color(0x999900)), + CHARACTER("A letter"), + TEXT("Sequence of letters & spaces", new Color(0x9999ff)), + // + // ========================================================================= + // This is the end of physical shapes. + // Next shapes are pure logical shapes, that CANNOT be inferred only from + // their physical characteristics. + // ========================================================================= + // + + // + // Shapes from shape sets -------------------------------------------------- + // + REPEAT_DOT("Repeat dot", DOT_set, new Color(0x0000ff)), + AUGMENTATION_DOT("Augmentation Dot", DOT_set), + STACCATO("Staccato dot", DOT_set), + FERMATA("Fermata", FERMATA_set), + FERMATA_BELOW("Fermata Below", FERMATA_set), + WHOLE_REST("Rest for whole measure", HW_REST_set), + HALF_REST("Rest for a 1/2", HW_REST_set), + TIME_SIX("Digit 6", TIME_69_set), + TIME_NINE("Digit 9", TIME_69_set), + FLAG_1("Single flag down", FLAG_1_set), + FLAG_1_UP("Single flag up", FLAG_1_set), + FLAG_2("Double flag down", FLAG_2_set), + FLAG_2_UP("Double flag up", FLAG_2_set), + FLAG_3("Triple flag down", FLAG_3_set), + FLAG_3_UP("Triple flag up", FLAG_3_set), + FLAG_4("Quadruple flag down", FLAG_4_set), + FLAG_4_UP("Quadruple flag up", FLAG_4_set), + FLAG_5("Quintuple flag down", FLAG_5_set), + FLAG_5_UP("Quintuple flag up", FLAG_5_set), + CRESCENDO("Crescendo", WEDGE_set), + DECRESCENDO("Decrescendo", WEDGE_set), + TURN("Turn", TURN_set), + INVERTED_TURN("Inverted Turn", TURN_set), + TURN_UP("Turn Up", TURN_set), + // + // Bars -------------------------------------------------------------------- + // + PART_DEFINING_BARLINE("Bar line that defines a part"), + THIN_BARLINE("Thin bar line"), + THICK_BARLINE("Thick bar line"), + DOUBLE_BARLINE("Double thin bar line"), + FINAL_BARLINE("Thin / Thick bar line"), + REVERSE_FINAL_BARLINE("Thick / Thin bar line"), + LEFT_REPEAT_SIGN("Thick / Thin bar line + Repeat dots"), + RIGHT_REPEAT_SIGN("Repeat dots + Thin / Thick bar line"), + BACK_TO_BACK_REPEAT_SIGN("Repeat dots + Thin / Thick / Thin + REPEAT_DOTS"), + ENDING("Alternate ending"), + // + // Miscellaneous + // + REPEAT_DOT_PAIR("Pair of repeat dots"), + NOISE("Too small stuff", new Color(0xcccccc)), + STAFF_LINE("Staff Line", new Color(0xffffcc)), + LEDGER("Ledger", new Color(0xaaaaaa)), + ENDING_HORIZONTAL("Horizontal part of ending"), + ENDING_VERTICAL("Vertical part of ending"), + // + // Stems + // + STEM("Stem", new Color(0xccff66)), + // + // Key signatures ---------------------------------------------------------- + // + KEY_FLAT_1("One Flat"), + KEY_SHARP_1("One Sharp"), + // + // Other stuff ------------------------------------------------------------- + // + FORWARD("To indicate a forward"), + NON_DRAGGABLE("Non draggable shape"), + GLYPH_PART("Part of a larger glyph"), + CUSTOM_TIME("Time signature defined by user"), + NO_LEGAL_TIME("No Legal Time Shape"); + // + // ========================================================================= + // This is the end of shape enumeration + // ========================================================================= + // + + /** Usual logger utility */ + private static final Logger logger = LoggerFactory.getLogger(Shape.class); + + /** Last physical shape */ + public static final Shape LAST_PHYSICAL_SHAPE = TEXT; + + /** A comparator based on shape name */ + public static Comparator alphaComparator = new Comparator() + { + @Override + public int compare (Shape o1, + Shape o2) + { + return o1.name() + .compareTo(o2.name()); + } + }; + + //~ Instance fields -------------------------------------------------------- + // + /** Explanation of the glyph shape */ + private final String description; + + /** Potential related symbol */ + private ShapeSymbol symbol; + + /** Potential related decorated symbol for menus */ + private ShapeSymbol decoratedSymbol; + + /** Remember the fact that this shape has no related symbol */ + private boolean hasNoSymbol; + + /** Remember the fact that this shape has no related decorated symbol */ + private boolean hasNoDecoratedSymbol; + + /** Potential related physical shape */ + private Shape physicalShape; + + /** Related color */ + private Color color; + + /** Related color constant */ + private Constant.Color constantColor; + + //-------------------------------------------------------------------------- + // + //-------// + // Shape // + //-------// + Shape () + { + this("", null, null); + } + + //-------// + // Shape // + //-------// + Shape (String description) + { + this(description, null, null); + } + + //-------// + // Shape // + //-------// + Shape (String description, + Color color) + { + this(description, null, color); + } + + //-------// + // Shape // + //-------// + Shape (String description, + Shape physicalShape) + { + this(description, physicalShape, null); + } + + //-------// + // Shape // + //-------// + Shape (String description, + Shape physicalShape, + Color color) + { + this.description = description; + this.physicalShape = physicalShape; + this.color = color; + + // Create the underlying constant + constantColor = new Constant.Color( + getClass().getName(), // Unit + name() + ".color", // Name + Constant.Color.encodeColor(color != null ? color : Color.BLACK), + "Color for shape " + name()); + } + + //-------------------------------------------------------------------------- + //---------------// + // isMeasureRest // + //---------------// + /** + * Check whether the shape is a whole (or multi) rest, for which + * no duration can be specified. + * + * @return true if whole or multi rest + */ + public boolean isMeasureRest () + { + return (this == WHOLE_REST) || (this == BREVE_REST) + || (this == LONG_REST); + } + + //--------------// + // isPersistent // + //--------------// + /** + * Report whether the impact of this shape persists across system + * (actually measure) borders (clefs, time signatures, key signatures). + * Based on just the shape, we cannot tell whether an accidental is part of + * a key signature or not, so we take a conservative approach. + * + * @return true if persistent, false otherwise + */ + public boolean isPersistent () + { + return ShapeSet.Clefs.contains(this) || ShapeSet.Times.contains(this) + || ShapeSet.Accidentals.contains(this); + } + + //--------// + // isText // + //--------// + /** + * Check whether the shape is a text (or a simple character). + * + * @return true if text or character + */ + public boolean isText () + { + return (this == TEXT) || (this == CHARACTER); + } + + //--------------// + // isSharpBased // + //--------------// + /** + * Check whether the shape is a sharp or a key-sig sequence of + * sharps. + * + * @return true if sharp or sharp key sig + */ + public boolean isSharpBased () + { + return (this == SHARP) || ShapeSet.SharpKeys.contains(this); + } + + //-------------// + // isFlatBased // + //-------------// + /** + * Check whether the shape is a flat or a key-sig sequence of + * flats. + * + * @return true if flat or flat key sig + */ + public boolean isFlatBased () + { + return (this == FLAT) || ShapeSet.FlatKeys.contains(this); + } + + //-------------// + // isTrainable // + //-------------// + /** + * Report whether this shape can be used to train an evaluator. + * + * @return true if trainable, false otherwise + */ + public boolean isTrainable () + { + return ordinal() <= LAST_PHYSICAL_SHAPE.ordinal(); + } + + //-------------// + // isWellKnown // + //-------------// + /** + * Report whether this shape is well known, that is a non-garbage + * symbol. + * + * @return true if non-garbage, false otherwise + */ + public boolean isWellKnown () + { + return (this != NO_LEGAL_TIME) && (this != GLYPH_PART) + && (this != NOISE); + } + + //----------------// + // getDescription // + //----------------// + /** + * Report a user-friendly description of this shape. + * + * @return the shape description + */ + public String getDescription () + { + if (description == null) { + return toString(); // Could be improved + } else { + return description; + } + } + + //----------// + // getColor // + //----------// + /** + * Report the color assigned to the shape, if any. + * + * @return the related color, or null + */ + public java.awt.Color getColor () + { + return color; + } + + //----------// + // setColor // + //----------// + /** + * Assign a color for this shape. + * + * @param color the display color + */ + public void setColor (java.awt.Color color) + { + this.color = color; + } + + //------------------// + // setConstantColor // + //------------------// + /** + * Define a specific color for the shape. + * + * @param color the specified color + */ + public void setConstantColor (Color color) + { + constantColor.setValue(color); + setColor(color); + } + + //------------------// + // createShapeColor // + //------------------// + void createShapeColor (Color color) + { + // Assign the shape display color + if (!constantColor.isSourceValue()) { + setColor(constantColor.getValue()); // Use the shape specific color + } else if (this.color == null) { + setColor(color); // Use the provided (range) default color + } + } + + //-----------// + // getSymbol // + //-----------// + /** + * Report the symbol related to the shape, if any. + * + * @return the related symbol, or null + */ + public ShapeSymbol getSymbol () + { + if (hasNoSymbol) { + return null; + } + + if (symbol == null) { + symbol = Symbols.getSymbol(this); + + if (symbol == null) { + hasNoSymbol = true; + } + } + + return symbol; + } + + //-----------// + // setSymbol // + //-----------// + /** + * Assign a symbol to this shape. + * + * @param symbol the assigned symbol, which may be null + */ + public void setSymbol (ShapeSymbol symbol) + { + this.symbol = symbol; + } + + //--------------------// + // getDecoratedSymbol // + //--------------------// + /** + * Report the symbol to use for menu items. + * + * @return the shape symbol, with decorations if any + */ + public ShapeSymbol getDecoratedSymbol () + { + // Avoid a new search, just use the undecorated symbol instead + if (hasNoDecoratedSymbol) { + return getSymbol(); + } + + // Try to build / load a decorated symbol + if (decoratedSymbol == null) { + setDecoratedSymbol(Symbols.getSymbol(this, true)); + + if (decoratedSymbol == null) { + hasNoDecoratedSymbol = true; + + return getSymbol(); + } + } + + // Simply return the cached decorated symbol + return decoratedSymbol; + } + + //--------------------// + // setDecoratedSymbol // + //--------------------// + /** + * Assign a decorated symbol to this shape. + * + * @param decoratedSymbol the assigned decorated symbol, which may be null + */ + public void setDecoratedSymbol (ShapeSymbol decoratedSymbol) + { + this.decoratedSymbol = decoratedSymbol; + } + + //------------------// + // getPhysicalShape // + //------------------// + /** + * Report the shape to use for training or precise drawingw. + * + * @return the related physical shape, if different + */ + public Shape getPhysicalShape () + { + if (physicalShape != null) { + return physicalShape; + } else { + return this; + } + } + + //-------------// + // isDraggable // + //-------------// + /** + * Report whether this shape can be dragged (in a DnD gesture). + * + * @return true if draggable + */ + public boolean isDraggable () + { + return getPhysicalShape() + .getSymbol() != null; + } + + //-----------------// + // dumpShapeColors // + //-----------------// + /** + * Dump the color of every shape. + */ + public static void dumpShapeColors () + { + List names = new ArrayList<>(); + + for (Shape shape : Shape.values()) { + names.add( + shape + " " + Constant.Color.encodeColor(shape.getColor())); + } + + Collections.sort(names); + + for (String str : names) { + System.out.println(str); + } + } +} diff --git a/src/main/omr/glyph/ShapeChecker.java b/src/main/omr/glyph/ShapeChecker.java new file mode 100644 index 0000000..df73458 --- /dev/null +++ b/src/main/omr/glyph/ShapeChecker.java @@ -0,0 +1,1365 @@ +//----------------------------------------------------------------------------// +// // +// S h a p e C h e c k e r // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.glyph; + +import omr.constant.Constant; +import omr.constant.ConstantSet; +import static omr.glyph.Shape.*; +import static omr.glyph.ShapeSet.*; +import omr.glyph.facets.Glyph; + +import omr.grid.StaffInfo; + +import omr.run.Orientation; + +import omr.score.entity.Barline; +import omr.score.entity.Clef; +import omr.score.entity.Measure; +import omr.score.entity.ScoreSystem; +import omr.score.entity.Staff; +import omr.score.entity.SystemPart; + +import omr.sheet.Scale; +import omr.sheet.Sheet; +import omr.sheet.SystemInfo; + +import omr.util.HorizontalSide; +import omr.util.Predicate; + +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.EnumMap; +import java.util.EnumSet; +import java.util.List; + +/** + * Class {@code ShapeChecker} gathers additional specific shape checks, + * still working on symbols in isolation from other symbols, meant to + * complement the work done by a shape evaluator. + * + *

+ * Typically, physical shapes (the *_set shape names) must be mapped to + * the right logical shapes using proper additional tests.

+ * + *

+ * Checks are made on the glyph only, the only knowledge about current glyph + * environment being its staff-based pitch position and the attached stems and + * ledgers.

+ * + *

+ * Checks made in relation with other symbols are not handled here (because + * the other symbols may not have been recognized yet). Such more elaborated + * checks are the purpose of {@link omr.glyph.pattern.PatternsChecker}.

+ * + * @author Hervé Bitteur + */ +public class ShapeChecker +{ + //~ Static fields/initializers --------------------------------------------- + + /** Specific application parameters */ + private static final Constants constants = new Constants(); + + /** Usual logger utility */ + private static final Logger logger = LoggerFactory.getLogger(ShapeChecker.class); + + /** Singleton */ + private static ShapeChecker INSTANCE; + + /** Small dynamics with no 'P' or 'F' */ + private static final EnumSet SmallDynamics = EnumSet.copyOf( + shapesOf( + DYNAMICS_CHAR_M, + DYNAMICS_CHAR_R, + DYNAMICS_CHAR_S, + DYNAMICS_CHAR_Z)); + + /** Medium dynamics with a 'P' (but no 'F') */ + private static final EnumSet MediumDynamics = EnumSet.copyOf( + shapesOf(DYNAMICS_MP, DYNAMICS_P, DYNAMICS_PP, DYNAMICS_PPP)); + + /** Tall dynamics with an 'F' */ + private static final EnumSet TallDynamics = EnumSet.copyOf( + shapesOf( + DYNAMICS_F, + DYNAMICS_FF, + DYNAMICS_FFF, + DYNAMICS_FP, + DYNAMICS_FZ, + DYNAMICS_MF, + DYNAMICS_RF, + DYNAMICS_RFZ, + DYNAMICS_SF, + DYNAMICS_SFFZ, + DYNAMICS_SFP, + DYNAMICS_SFPP, + DYNAMICS_SFZ)); + + //~ Instance fields -------------------------------------------------------- + /** Map of Shape => Sequence of checkers */ + private final EnumMap> checkerMap; + + /** Checker that can be used on its own. */ + private Checker stemChecker; + + //~ Constructors ----------------------------------------------------------- + //--------------// + // ShapeChecker // + //--------------// + private ShapeChecker () + { + checkerMap = new EnumMap<>(Shape.class); + registerChecks(); + } + + //~ Methods ---------------------------------------------------------------- + //-------------// + // getInstance // + //-------------// + public static ShapeChecker getInstance () + { + if (INSTANCE == null) { + INSTANCE = new ShapeChecker(); + } + + return INSTANCE; + } + + //-----------// + // checkStem // + //-----------// + /** + * Basic check for a stem candidate, using gap to closest staff. + * + * @param system containing system + * @param glyph stem candidate + * @return true if OK + */ + public boolean checkStem (SystemInfo system, + Glyph glyph) + { + return stemChecker.check(system, null, glyph, null); + } + + //----------// + // annotate // + //----------// + /** + * Run a series of checks on the provided glyph, based on the + * candidate shape, and annotate the evaluation accordingly. + * This annotation can even change the shape itself, thus allowing a move + * from physical shape (such as WEDGE_set) to proper logical shape + * (CRESCENDO or DECRESCENDO). + * + * @param system the containing system + * @param eval the evaluation to populate + * @param glyph the glyph to check for a shape + * @param features the glyph features + */ + public void annotate (SystemInfo system, + Evaluation eval, + Glyph glyph, + double[] features) + { + if (!constants.applySpecificCheck.getValue()) { + return; + } + + // if (glyph.isVip()) { + // logger.info("Checking " + glyph); + // } + // + Collection checks = checkerMap.get(eval.shape); + + if (checks == null) { + return; + } + + for (Checker checker : checks) { + if (!(checker.check(system, eval, glyph, features))) { + if (eval.failure != null) { + eval.failure = new Evaluation.Failure( + checker.name + ":" + eval.failure); + } else { + eval.failure = new Evaluation.Failure(checker.name); + } + + return; + } + } + } + + //-------// + // relax // + //-------// + /** + * Take into account the fact that the provided glyph has been + * (certainly manually) assigned the provided shape. + * So update the tests internals accordingly. + * + * @param shape the assigned shape + * @param glyph the glyph at hand + * @param features the glyph features + * @param sheet the containing sheet + */ + public void relax (Shape shape, + Glyph glyph, + double[] features, + Sheet sheet) + { + Collection checks = checkerMap.get(shape); + + if (checks == null) { + return; + } + + for (Checker checker : checks) { + checker.relax(shape, glyph, features, sheet); + } + } + + //------------// + // addChecker // + //------------// + /** + * Add a checker to a series of shapes. + * + * @param checker the checker to add + * @param shapes the shape(s) for which the check applies + */ + private void addChecker (Checker checker, + Shape... shapes) + { + for (Shape shape : shapes) { + Collection checks = checkerMap.get(shape); + + if (checks == null) { + checks = new ArrayList<>(); + checkerMap.put(shape, checks); + } + + checks.add(checker); + } + } + + //------------// + // addChecker // + //------------// + /** + * Add a checker to a series of shape ranges. + * + * @param checker the checker to add + * @param shapeRanges the shape range(s) to which the check applies + */ + private void addChecker (Checker checker, + ShapeSet... shapeRanges) + { + for (ShapeSet range : shapeRanges) { + addChecker(checker, range.getShapes().toArray(new Shape[0])); + } + } + + //--------------// + // correctShape // + //--------------// + private boolean correctShape (SystemInfo system, + Glyph glyph, + Evaluation eval, + Shape newShape) + { + if (eval.shape != newShape) { + // logger.info( + // system.getLogPrefix() + "G#" + glyph.getId() + " " + + // eval.shape + " -> " + newShape); + eval.shape = newShape; + } + + return true; + } + + //------------// + // logLogical // + //------------// + /** + * Meant for debugging the mapping from physical to logical shape. + * + * @param system related system + * @param glyph the glyph at hand + * @param eval the physical evaluation + * @param newShape the chosen logical shape + */ + private void logLogical (SystemInfo system, + Glyph glyph, + Evaluation eval, + Shape newShape) + { + // For debugging only + if (eval.grade >= 0.1) { + logger.info("{}{} {} weight:{} {} corrected as {}", + system.getLogPrefix(), glyph, eval, glyph.getWeight(), + glyph.getBounds(), newShape); + } + } + + //----------------// + // registerChecks // + //----------------// + /** + * Populate the checkers map. + */ + private void registerChecks () + { + // // General constraint check on weight, width, height + // new Checker("Constraint", allPhysicalShapes) { + // @Override + // public boolean check (SystemInfo system, + // Evaluation eval, + // Glyph glyph, + // double[] features) + // { + // if (!constants.applyConstraintsCheck.getValue()) { + // return true; + // } + // + // // Apply registered parameters constraints + // return GlyphRegression.getInstance() + // .constraintsMatched(features, eval); + // } + // + // @Override + // public void relax (Shape shape, + // Glyph glyph, + // double[] features, + // Sheet sheet) + // { + // // Here relax the constraints if so needed + // boolean extended = GlyphRegression.getInstance() + // .includeSample( + // features, + // shape); + // logger.info( + // "Constraints " + (extended ? "extended" : "included") + + // " for glyph#" + glyph.getId() + " as " + shape); + // + // // Record the glyph description to disk + // GlyphRepository.getInstance() + // .recordOneGlyph(glyph, sheet); + // } + // }; + new Checker("NotWithinWidth", allPhysicalShapes) + { + @Override + public boolean check (SystemInfo system, + Evaluation eval, + Glyph glyph, + double[] features) + { + // They must be within the abscissa bounds of the system + // Except a few shapes + Shape shape = eval.shape; + + if ((shape == BRACKET) + || (shape == BRACE) + || (shape == TEXT) + || (shape == CHARACTER)) { + return true; + } + + Rectangle glyphBox = glyph.getBounds(); + + if (((glyphBox.x + glyphBox.width) < system.getLeft()) + || (glyphBox.x > system.getRight())) { + return false; + } + + return true; + } + }; + + new Checker("MeasureRest", HW_REST_set) + { + @Override + public boolean check (SystemInfo system, + Evaluation eval, + Glyph glyph, + double[] features) + { + int pp = (int) Math.rint(2 * glyph.getPitchPosition()); + + if (pp == -1) { + eval.shape = Shape.HALF_REST; + + return true; + } else if (pp == -3) { + eval.shape = Shape.WHOLE_REST; + + return true; + } else { + eval.failure = new Evaluation.Failure("pitch"); + + return false; + } + } + }; + + new Checker("NotWithinStaffHeight", Clefs) + { + @Override + public boolean check (SystemInfo system, + Evaluation eval, + Glyph glyph, + double[] features) + { + // Must be within staff height + return Math.abs(glyph.getPitchPosition()) < 4; + } + }; + + new Checker("WithinStaffHeight", Dynamics) + { + @Override + public boolean check (SystemInfo system, + Evaluation eval, + Glyph glyph, + double[] features) + { + // Must be outside staff height + return Math.abs(glyph.getPitchPosition()) > 4; + } + }; + + new Checker("TooFarFromLeftBar", Keys) + { + @Override + public boolean check (SystemInfo system, + Evaluation eval, + Glyph glyph, + double[] features) + { + // They must be rather close to the left side of the measure + ScoreSystem scoreSystem = system.getScoreSystem(); + Scale scale = scoreSystem.getScale(); + double maxKeyXOffset = scale.toPixels( + constants.maxKeyXOffset); + Rectangle box = glyph.getBounds(); + Point point = box.getLocation(); + SystemPart part = scoreSystem.getPartAt(point); + Measure measure = part.getMeasureAt(point); + + if (measure == null) { + return true; + } + + Barline insideBar = measure.getInsideBarline(); + Staff staff = part.getStaffAt(point); + if (staff == null) { + return false; + } + Clef clef = measure.getFirstMeasureClef(staff.getId()); + int start = (clef != null) + ? (clef.getBox().x + clef.getBox().width) + : ((insideBar != null) + ? insideBar.getLeftX() : measure.getLeftX()); + + return (point.x - start) <= maxKeyXOffset; + } + }; + + new Checker("CommonCutTime", COMMON_TIME) + { + private Predicate stemPredicate = new Predicate() + { + @Override + public boolean check (Glyph entity) + { + return entity.getShape() == Shape.STEM; + } + }; + + @Override + public boolean check (SystemInfo system, + Evaluation eval, + Glyph glyph, + double[] features) + { + // COMMON_TIME shape is easily confused with CUT_TIME + // Check presence of a "pseudo-stem" + Rectangle box = glyph.getBounds(); + box.grow(-box.width / 4, 0); + + List neighbors = system.lookupIntersectedGlyphs( + box, + glyph); + + if (Glyphs.contains(neighbors, stemPredicate)) { + eval.shape = Shape.CUT_TIME; + } + + return true; + } + }; + + new Checker("Hook", BEAM_HOOK) + { + @Override + public boolean check (SystemInfo system, + Evaluation eval, + Glyph glyph, + double[] features) + { + // Check we have exactly 1 stem + if (glyph.getStemNumber() != 1) { + eval.failure = new Evaluation.Failure("stem!=1"); + + return false; + } + + // Hook slope is not reliable, so this test is disabled + // if (!validBeamHookSlope(glyph)) { + // eval.failure = new Evaluation.Failure("slope"); + // + // return false; + // } + return true; + } + }; + + new Checker("Beams", shapesOf(BEAM, BEAM_2, BEAM_3)) + { + @Override + public boolean check (SystemInfo system, + Evaluation eval, + Glyph glyph, + double[] features) + { + Integer singleThickness = system.getScoreSystem().getScale(). + getMainBeam(); + + if (singleThickness != null) { + // Check we have thickness consistent with the number of + // beams (since we know single beam thickness) + double meanThickness = glyph.getMeanThickness( + Orientation.HORIZONTAL); + + int nb = (int) Math.rint( + meanThickness / singleThickness); + + switch (nb) { + case 1: + return correctShape(system, glyph, eval, BEAM); + + case 2: + return correctShape(system, glyph, eval, BEAM_2); + + case 3: + return correctShape(system, glyph, eval, BEAM_3); + + default: + ///logger.warn("Bad beam #" + glyph.getId() + " nb:" + nb); + eval.failure = new Evaluation.Failure("beamThickness"); + + return false; + } + } else { + return true; + } + } + }; + + // Shapes that require a stem on the left side + new Checker( + "noLeftStem", + shapesOf(FlagSets, shapesOf(Flags.getShapes()))) + { + @Override + public boolean check (SystemInfo system, + Evaluation eval, + Glyph glyph, + double[] features) + { + return glyph.getStem(HorizontalSide.LEFT) != null; + } + }; + + // Shapes that require a stem nearby + new Checker("noStem", StemSymbols) + { + @Override + public boolean check (SystemInfo system, + Evaluation eval, + Glyph glyph, + double[] features) + { + return glyph.getStemNumber() >= 1; + } + }; + + new Checker("Text", TEXT, CHARACTER) + { + @Override + public boolean check (SystemInfo system, + Evaluation eval, + Glyph glyph, + double[] features) + { + // Check reasonable height (Cannot be too tall when close to staff) + double maxHeight = (Math.abs(glyph.getPitchPosition()) >= constants.minTitlePitchPosition. + getValue()) + ? constants.maxTitleHeight.getValue() + : constants.maxLyricsHeight.getValue(); + + if (glyph.getNormalizedHeight() >= maxHeight) { + eval.failure = new Evaluation.Failure("tooHigh"); + + return false; + } + + // // Check there is no huge horizontal gap between parts + // if (hugeGapBetweenParts(glyph)) { + // eval.failure = new Evaluation.Failure("gaps"); + // + // return false; + // } + return true; + } + // /** + // * Browse the collection of provided glyphs to make sure there + // * is no huge horizontal gap included + // * @param glyphs the collection of glyphs that compose the text + // * candidate + // * @param sheet needed for scale of the context + // * @return true if gap found + // */ + // private boolean hugeGapBetweenParts (Glyph compound) + // { + // if (compound.getParts() + // .isEmpty()) { + // return false; + // } + // + // // Sort glyphs by abscissa + // List glyphs = new ArrayList( + // compound.getParts()); + // Collections.sort(glyphs, Glyph.abscissaComparator); + // + // final Scale scale = new Scale(glyphs.get(0).getInterline()); + // final int maxGap = scale.toPixels(constants.maxTextGap); + // int gapStart = 0; + // Glyph prev = null; + // + // for (Glyph glyph : glyphs) { + // Rectangle box = glyph.getBounds(); + // + // if (prev != null) { + // if ((box.x - gapStart) > maxGap) { + // if (logger.isDebugEnabled()) { + // logger.debug( + // "huge gap detected between glyphs #" + + // prev.getId() + " & " + glyph.getId()); + // } + // + // return true; + // } + // } + // + // prev = glyph; + // gapStart = (box.x + box.width) - 1; + // } + // + // return false; + // } + }; + + new Checker("FullTimeSig", FullTimes) + { + @Override + public boolean check (SystemInfo system, + Evaluation eval, + Glyph glyph, + double[] features) + { + double absPos = Math.abs(glyph.getPitchPosition()); + double maxDy = constants.maxTimePitchPositionMargin.getValue(); + + // A full time shape must be on 0 position + if (absPos > maxDy) { + eval.failure = new Evaluation.Failure("pitch"); + + return false; + } + + // Total height for a complete time sig is staff height + if (glyph.getNormalizedHeight() > 4.5) { + eval.failure = new Evaluation.Failure("tooHigh"); + + return false; + } + + return true; + } + }; + + new Checker("PartialTimeSig", TIME_69_set, PartialTimes) + { + @Override + public boolean check (SystemInfo system, + Evaluation eval, + Glyph glyph, + double[] features) + { + double absPos = Math.abs(glyph.getPitchPosition()); + double maxDy = constants.maxTimePitchPositionMargin.getValue(); + + // A partial time shape must be on -2 or +2 positions + if (Math.abs(absPos - 2) > maxDy) { + eval.failure = new Evaluation.Failure("pitch"); + + return false; + } + + return true; + } + }; + + new Checker("StaffGap", Notes.getShapes(), NoteHeads.getShapes(), + Rests.getShapes(), Dynamics.getShapes(), + Articulations.getShapes()) + { + @Override + public boolean check (SystemInfo system, + Evaluation eval, + Glyph glyph, + double[] features) + { + // A note / rest / dynamic cannot be too far from a staff + Point center = glyph.getAreaCenter(); + StaffInfo staff = system.getStaffAt(center); + + // Staff may be null when we are modifying system boundaries + if (staff == null) { + return false; + } + + int gap = staff.getGapTo(glyph); + int maxGap = system.getScoreSystem().getScale().toPixels( + constants.maxGapToStaff); + return gap <= maxGap; + } + }; + + stemChecker = new Checker("StaffStemGap", + shapesOf(shapesOf(STEM), + shapesOf(Beams.getShapes(), + Flags.getShapes(), + FlagSets))) + { + @Override + public boolean check (SystemInfo system, + Evaluation eval, + Glyph glyph, + double[] features) + { + // A beam / flag / stem cannot be too far from a staff + Point center = glyph.getAreaCenter(); + StaffInfo staff = system.getStaffAt(center); + // Staff may be null for a very long glyph across systems + if (staff == null) { + return false; + } + int gap = staff.getGapTo(glyph); + int maxGap = system.getScoreSystem().getScale().toPixels( + constants.maxStemGapToStaff); + return gap <= maxGap; + } + }; + + new Checker("SmallDynamics", SmallDynamics) + { + @Override + public boolean check (SystemInfo system, + Evaluation eval, + Glyph glyph, + double[] features) + { + // Check height + return glyph.getNormalizedHeight() <= constants.maxSmallDynamicsHeight. + getValue(); + } + }; + + new Checker("MediumDynamics", MediumDynamics) + { + @Override + public boolean check (SystemInfo system, + Evaluation eval, + Glyph glyph, + double[] features) + { + // Check height + return glyph.getNormalizedHeight() <= constants.maxMediumDynamicsHeight. + getValue(); + } + }; + + new Checker("TallDynamics", TallDynamics) + { + @Override + public boolean check (SystemInfo system, + Evaluation eval, + Glyph glyph, + double[] features) + { + // Check height + return glyph.getNormalizedHeight() <= constants.maxTallDynamicsHeight. + getValue(); + } + }; + + new Checker("BelowStaff", Pedals) + { + @Override + public boolean check (SystemInfo system, + Evaluation eval, + Glyph glyph, + double[] features) + { + // Pedal marks must be below the staff + return glyph.getPitchPosition() > 4; + } + }; + + new Checker("Tuplet", Tuplets) + { + @Override + public boolean check (SystemInfo system, + Evaluation eval, + Glyph glyph, + double[] features) + { + // Tuplets cannot be too far from a staff + if (Math.abs(glyph.getPitchPosition()) > constants.maxTupletPitchPosition. + getValue()) { + eval.failure = new Evaluation.Failure("pitch"); + + return false; + } + + // // Simply check the tuplet character via OCR, if available + // // Nota: We must avoid multiple OCR calls on the same glyph + // if (Language.getOcr() + // .isAvailable()) { + // if (glyph.isTransient()) { + // glyph = system.registerGlyph(glyph); + // } + // + // BasicContent textInfo = glyph.getTextInfo(); + // OcrLine line; + // + // if (textInfo.getOcrContent() == null) { + // String language = system.getScoreSystem() + // .getScore() + // .getLanguage(); + // List lines = textInfo.recognizeGlyph( + // language); + // + // if ((lines != null) && !lines.isEmpty()) { + // line = lines.get(0); + // textInfo.setOcrInfo(language, line); + // } + // } + // + // line = textInfo.getOcrLine(); + // + // if (line != null) { + // String str = line.value; + // Shape shape = eval.shape; + // + // if (shape == TUPLET_THREE) { + // if (str.equals("3")) { + // return true; + // } + // + // //eval.shape = CHARACTER; + // } + // + // if (shape == TUPLET_SIX) { + // if (str.equals("6")) { + // return true; + // } + // + // //eval.shape = CHARACTER; + // } + // + // eval.failure = new Evaluation.Failure("ocr"); + // + // return false; + // } + // } + return true; + } + }; + + new Checker("LongRest", LONG_REST) + { + @Override + public boolean check (SystemInfo system, + Evaluation eval, + Glyph glyph, + double[] features) + { + // Must be centered on pitch position 0 + if (Math.abs(glyph.getPitchPosition()) > 0.5) { + eval.failure = new Evaluation.Failure("pitch"); + + return false; + } + + return true; + } + }; + + new Checker("Breve", BREVE_REST) + { + @Override + public boolean check (SystemInfo system, + Evaluation eval, + Glyph glyph, + double[] features) + { + // Must be centered on pitch position -1 + if (Math.abs(glyph.getPitchPosition() + 1) > 0.5) { + eval.failure = new Evaluation.Failure("pitch"); + + return false; + } + + return true; + } + }; + + new Checker("Braces", BRACE, BRACKET) + { + @Override + public boolean check (SystemInfo system, + Evaluation eval, + Glyph glyph, + double[] features) + { + // Must be centered on left of part barline + Rectangle box = glyph.getBounds(); + Point left = new Point( + box.x, + box.y + (box.height / 2)); + + if (left.x > system.getLeft()) { + eval.failure = new Evaluation.Failure("notOnLeft"); + + return false; + } + + // Make sure at least a staff interval is embraced + boolean embraced = false; + int intervalTop = Integer.MIN_VALUE; + + for (StaffInfo staff : system.getStaves()) { + if (intervalTop != Integer.MIN_VALUE) { + int intervalBottom = staff.getFirstLine().yAt(box.x); + + if ((intervalTop >= box.y) + && (intervalBottom <= (box.y + box.height))) { + embraced = true; // Ok for this one + + break; + } + } + + intervalTop = staff.getLastLine().yAt(box.x); + } + + if (!embraced) { + eval.failure = new Evaluation.Failure( + "noStaffEmbraced"); + + return false; + } + + return true; + } + }; + + new Checker("WholeSansLedgers", WHOLE_NOTE) + { + @Override + public boolean check (SystemInfo system, + Evaluation eval, + Glyph glyph, + double[] features) + { + // Check that whole notes are not too far from staves + // without ledgers + Point point = glyph.getAreaCenter(); + StaffInfo staff = system.getStaffAt(point); + if (staff == null) { + return false; + } + double pitch = staff.pitchPositionOf(point); + + if (Math.abs(pitch) <= 6) { + return true; + } + + return staff.getClosestLedger(point) != null; + } + }; + + new Checker("SystemTop", DAL_SEGNO, DA_CAPO, SEGNO, CODA, BREATH_MARK) + { + @Override + public boolean check (SystemInfo system, + Evaluation eval, + Glyph glyph, + double[] features) + { + // Check that these markers are just above first staff + Point point = glyph.getAreaCenter(); + StaffInfo staff = system.getStaffAt(point); + + if (staff != system.getFirstStaff()) { + return false; + } + + double pitch = staff.pitchPositionOf(point); + + return pitch <= -5; + } + }; + + new Checker("Fermata_set", FERMATA_set) + { + @Override + public boolean check (SystemInfo system, + Evaluation eval, + Glyph glyph, + double[] features) + { + // Use moment n21 to differentiate between V & ^ + // TBD: We could use pitch position as well? + double n21 = glyph.getGeometricMoments().getN21(); + Shape newShape = (n21 > 0) ? FERMATA : FERMATA_BELOW; + + ///logLogical(system, glyph, eval, newShape); + eval.shape = newShape; + + return true; + } + }; + + new Checker("FLAG_*_set", FlagSets) + { + @Override + public boolean check (SystemInfo system, + Evaluation eval, + Glyph glyph, + double[] features) + { + Shape newShape = null; + boolean covar = glyph.getGeometricMoments().getN11() > 0; + + switch (eval.shape) { + case FLAG_1_set: + newShape = covar ? FLAG_1 : FLAG_1_UP; + + break; + + case FLAG_2_set: + newShape = covar ? FLAG_2 : FLAG_2_UP; + + break; + + case FLAG_3_set: + newShape = covar ? FLAG_3 : FLAG_3_UP; + + break; + + case FLAG_4_set: + newShape = covar ? FLAG_4 : FLAG_4_UP; + + break; + + case FLAG_5_set: + newShape = covar ? FLAG_5 : FLAG_5_UP; + + break; + } + + ///logLogical(system, glyph, eval, newShape); + eval.shape = newShape; + + return true; + } + }; + + new Checker("TIME_69_set", TIME_69_set) + { + @Override + public boolean check (SystemInfo system, + Evaluation eval, + Glyph glyph, + double[] features) + { + // Use moment n12 to differentiate between <(6) & >(9) + double n12 = glyph.getGeometricMoments().getN12(); + Shape newShape = (n12 > 0) ? TIME_NINE : TIME_SIX; + ///logLogical(system, glyph, eval, newShape); + eval.shape = newShape; + + return true; + } + }; + + new Checker("TURN_set", TURN_set) + { + @Override + public boolean check (SystemInfo system, + Evaluation eval, + Glyph glyph, + double[] features) + { + Shape newShape; + + // Use aspect to detect turn_up + double aspect = glyph.getAspect(Orientation.VERTICAL); + + if (aspect > 1) { + newShape = TURN_UP; + } else { + // Use xy covariance + boolean covar = glyph.getGeometricMoments().getN11() > 0; + + newShape = covar ? TURN : INVERTED_TURN; + } + + ///logLogical(system, glyph, eval, newShape); + eval.shape = newShape; + + return true; + } + }; + + new Checker("Wedge_set", WEDGE_set) + { + @Override + public boolean check (SystemInfo system, + Evaluation eval, + Glyph glyph, + double[] features) + { + // Use moment n12 to differentiate between < & > + double n12 = glyph.getGeometricMoments().getN12(); + Shape newShape = (n12 > 0) ? CRESCENDO : DECRESCENDO; + + ///logLogical(system, glyph, eval, newShape); + eval.shape = newShape; + + return true; + } + }; + } + + //~ Inner Classes ---------------------------------------------------------- + //---------// + // Checker // + //---------// + /** + * A checker runs a specific check for a given glyph with respect to + * a collection of candidate shapes. + */ + private abstract class Checker + { + //~ Instance fields ---------------------------------------------------- + + /** Unique name for this check */ + public final String name; + + //~ Constructors ------------------------------------------------------- + public Checker (String name, + Shape... shapes) + { + this.name = name; + addChecker(this, shapes); + } + + public Checker (String name, + Collection shapes) + { + this.name = name; + addChecker(this, shapes.toArray(new Shape[0])); + } + + public Checker (String name, + Collection... shapes) + { + this.name = name; + Collection allShapes = new ArrayList<>(); + for (Collection col : shapes) { + allShapes.addAll(col); + } + addChecker(this, allShapes.toArray(new Shape[allShapes.size()])); + } + + public Checker (String name, + ShapeSet... shapeSets) + { + this.name = name; + addChecker(this, shapeSets); + } + + public Checker (String name, + Shape shape, + Collection collection) + { + this.name = name; + + List all = new ArrayList<>(); + all.add(shape); + + all.addAll(collection); + + addChecker(this, all.toArray(new Shape[0])); + } + + public Checker (String name, + Shape shape) + { + this.name = name; + + List all = new ArrayList<>(); + all.add(shape); + + addChecker(this, all.toArray(new Shape[0])); + } + + //~ Methods ------------------------------------------------------------ + /** + * Run the specific test. + * + * @param system the containing system + * @param eval the partially-filled evaluation (eval.shape is an + * input/output, eval.grade and eval.failure are outputs) + * @param glyph the glyph at hand + * @param features the glyph features + * @return true if OK, false otherwise + */ + public abstract boolean check (SystemInfo system, + Evaluation eval, + Glyph glyph, + double[] features); + + /** + * Take into account the fact that the provided glyph has been + * (certainly manually) assigned the provided shape. + * So update the test internals accordingly. + * + * @param shape the assigned shape + * @param glyph the glyph at hand + * @param features the glyph features + * @param sheet the containing sheet + */ + public void relax (Shape shape, + Glyph glyph, + double[] features, + Sheet sheet) + { + // Void by default + } + + @Override + public String toString () + { + return name; + } + } + + //-----------// + // Constants // + //-----------// + private static final class Constants + extends ConstantSet + { + //~ Instance fields ---------------------------------------------------- + + Constant.Boolean applySpecificCheck = new Constant.Boolean( + true, + "Should we apply specific checks on shape candidates?"); + + Constant.Boolean applyConstraintsCheck = new Constant.Boolean( + true, + "Should we apply constraints checks on shape candidates?"); + + Scale.Fraction maxTitleHeight = new Scale.Fraction( + 4d, + "Maximum normalized height for a title text"); + + Scale.Fraction maxLyricsHeight = new Scale.Fraction( + 2.5d, + "Maximum normalized height for a lyrics text"); + + Constant.Double minTitlePitchPosition = new Constant.Double( + "PitchPosition", + 15d, + "Minimum absolute pitch position for a title"); + + Constant.Double maxTupletPitchPosition = new Constant.Double( + "PitchPosition", + 15d, + "Minimum absolute pitch position for a tuplet"); + + Constant.Double maxTimePitchPositionMargin = new Constant.Double( + "PitchPosition", + 1d, + "Maximum absolute pitch position margin for a time signature"); + + Scale.Fraction maxTextGap = new Scale.Fraction( + 5.0, + "Maximum value for a horizontal gap between glyphs of the same text"); + + Scale.Fraction maxKeyXOffset = new Scale.Fraction( + 2, + "Maximum horizontal offset for a key since clef or measure start"); + + Scale.Fraction maxSmallDynamicsHeight = new Scale.Fraction( + 1.5, + "Maximum height for small dynamics (no p, no f)"); + + Scale.Fraction maxMediumDynamicsHeight = new Scale.Fraction( + 2, + "Maximum height for small dynamics (no p, no f)"); + + Scale.Fraction maxTallDynamicsHeight = new Scale.Fraction( + 2.5, + "Maximum height for small dynamics (no p, no f)"); + + Scale.Fraction maxGapToStaff = new Scale.Fraction( + 8, + "Maximum vertical gap between a note-like glyph and closest staff"); + + Scale.Fraction maxStemGapToStaff = new Scale.Fraction( + 12, + "Maximum vertical gap between a stem and closest staff"); + + } +} diff --git a/src/main/omr/glyph/ShapeDescription.java b/src/main/omr/glyph/ShapeDescription.java new file mode 100644 index 0000000..5a7c89a --- /dev/null +++ b/src/main/omr/glyph/ShapeDescription.java @@ -0,0 +1,137 @@ +//----------------------------------------------------------------------------// +// // +// S h a p e D e s c r i p t i o n // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.glyph; + +import omr.glyph.facets.Glyph; + +/** + * Class {@code ShapeDescription} builds the glyphs features to be used + * by an evaluator. + * + * @author Hervé Bitteur + */ +public abstract class ShapeDescription +{ + //~ Static fields/initializers --------------------------------------------- + + ///private static final Descriptor INSTANCE = new ShapeDescriptorGeo(); + private static final Descriptor INSTANCE = new ShapeDescriptorART(); + + //~ Constructors ----------------------------------------------------------- + private ShapeDescription () + { + } + + //~ Methods ---------------------------------------------------------------- + //--------------// + // boolAsDouble // + //--------------// + public static double boolAsDouble (boolean bool) + { + return bool ? 1.0 : 0.0; + } + + //----------// + // features // + //----------// + /** + * Report the features that describe a given glyph. + * + * @param glyph the glyph to describe + * @return the glyph shape features, an array of size length() + */ + public static double[] features (Glyph glyph) + { + return INSTANCE.features(glyph); + } + + //-------------------// + // getParameterIndex // + //-------------------// + /** + * Report the index of parameters for the provided label. + * + * @param label the provided label + * @return the parameter index + */ + public static int getParameterIndex (String label) + { + return INSTANCE.getFeatureIndex(label); + } + + //--------------------// + // getParameterLabels // + //--------------------// + /** + * Report the parameters labels. + * + * @return the array of parameters labels + */ + public static String[] getParameterLabels () + { + return INSTANCE.getFeatureLabels(); + } + + //--------// + // length // + //--------// + /** + * Report the number of features handled. + * + * @return the number of features + */ + public static int length () + { + return INSTANCE.length(); + } + + //~ Inner Interfaces ------------------------------------------------------- + // + //------------// + // Descriptor // + //------------// + public static interface Descriptor + { + //~ Methods ------------------------------------------------------------ + + /** + * Key method which gathers the various features meant to + * describe a glyph and recognize a shape. + * + * @param glyph the glyph to describe + * @return the glyph shape features, an array of size length() + */ + double[] features (Glyph glyph); + + /** + * Report the index of features for the provided label. + * + * @param label the provided label + * @return the feature index + */ + int getFeatureIndex (String label); + + /** + * Report the features labels. + * + * @return the array of feature labels + */ + String[] getFeatureLabels (); + + /** + * Report the number of features handled. + * + * @return the number of features + */ + int length (); + } +} diff --git a/src/main/omr/glyph/ShapeDescriptorART.java b/src/main/omr/glyph/ShapeDescriptorART.java new file mode 100644 index 0000000..4ef15cb --- /dev/null +++ b/src/main/omr/glyph/ShapeDescriptorART.java @@ -0,0 +1,141 @@ +//----------------------------------------------------------------------------// +// // +// S h a p e D e s c r i p t o r A R T // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.glyph; + +import omr.glyph.facets.Glyph; + +import omr.moments.ARTMoments; + +import omr.run.Orientation; + +import java.util.HashMap; +import java.util.Map; + +/** + * Class {@code ShapeDescriptorART} defines shape descriptions based + * on ART moments. + * + * @author Hervé Bitteur + */ +public class ShapeDescriptorART + implements ShapeDescription.Descriptor +{ + //~ Static fields/initializers --------------------------------------------- + + /** Number of orthogonal moments used */ + private static final int momentCount = -1 + + (ARTMoments.ANGULAR * ARTMoments.RADIAL); + + /** Use the orthogonal moments + weight + stems + aspect */ + private static final int length = momentCount + 3; + + //~ Methods ---------------------------------------------------------------- + //----------// + // features // + //----------// + @Override + public double[] features (Glyph glyph) + { + ARTMoments moments = glyph.getARTMoments(); + double[] ins = new double[length]; + int i = 0; + + // We take the orthogonal moments + for (int p = 0; p < ARTMoments.ANGULAR; p++) { + for (int r = 0; r < ARTMoments.RADIAL; r++) { + if ((p != 0) || (r != 0)) { + ins[i++] = moments.getMoment(p, r); + } + } + } + + // We append weight, stem count, aspect + ins[i++] = glyph.getNormalizedWeight(); + ins[i++] = glyph.getStemNumber(); + ins[i++] = glyph.getAspect(Orientation.VERTICAL); + + return ins; + } + + //-----------------// + // getFeatureIndex // + //-----------------// + @Override + public int getFeatureIndex (String label) + { + return LabelsHolder.indices.get(label); + } + + //------------------// + // getFeatureLabels // + //------------------// + @Override + public String[] getFeatureLabels () + { + return LabelsHolder.labels; + } + + //--------// + // length // + //--------// + @Override + public int length () + { + return length; + } + + //~ Inner Classes ---------------------------------------------------------- + //--------------// + // LabelsHolder // + //--------------// + /** + * Descriptive strings for glyph characteristics. + * + * NOTA: Keep in sync method {@link #features} + */ + private static class LabelsHolder + { + //~ Static fields/initializers ----------------------------------------- + + /** Label -> Index */ + public static final Map indices = new HashMap<>(); + + /** Index -> Label */ + public static final String[] labels = new String[length]; + + static { + int i = 0; + + // We take all the ART moments + for (int p = 0; p < ARTMoments.ANGULAR; p++) { + for (int r = 0; r < ARTMoments.RADIAL; r++) { + if ((p != 0) || (r != 0)) { + labels[i++] = String.format("F%02d%1d", p, r); + } + } + } + + // We append weight, stem count and aspect + labels[i++] = "weight"; + labels[i++] = "stemNb"; + labels[i++] = "aspect"; + + for (int j = 0; j < labels.length; j++) { + indices.put(labels[j], j); + } + } + + private LabelsHolder () + { + } + } +} diff --git a/src/main/omr/glyph/ShapeDescriptorGeo.java b/src/main/omr/glyph/ShapeDescriptorGeo.java new file mode 100644 index 0000000..511d6cf --- /dev/null +++ b/src/main/omr/glyph/ShapeDescriptorGeo.java @@ -0,0 +1,136 @@ +//----------------------------------------------------------------------------// +// // +// S h a p e D e s c r i p t o r G e o // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.glyph; + +import omr.glyph.facets.Glyph; + +import omr.moments.GeometricMoments; + +import omr.run.Orientation; + +import java.util.HashMap; +import java.util.Map; + +/** + * Class {@code ShapeDescriptorGeo} defines shape description based + * on Geometric moments. + * + * @author Hervé Bitteur + */ +public class ShapeDescriptorGeo + implements ShapeDescription.Descriptor +{ + //~ Static fields/initializers --------------------------------------------- + + /** Number of geometric moments used */ + private static final int momentCount = 10; + + /** Use the 10 first geometric moments + legder + stems + aspect */ + private static final int length = momentCount + 3; + + //~ Methods ---------------------------------------------------------------- + //----------// + // features // + //----------// + @Override + public double[] features (Glyph glyph) + { + double[] ins = new double[length]; + + // We take all the first moments + Double[] k = glyph.getGeometricMoments() + .getValues(); + + for (int i = 0; i < momentCount; i++) { + ins[i] = k[i]; + } + + // We append ledger presence, stem count and aspect + int i = momentCount; + /* 10 */ ins[i++] = ShapeDescription.boolAsDouble(glyph.isWithLedger()); + /* 11 */ ins[i++] = glyph.getStemNumber(); + /* 12 */ ins[i++] = glyph.getAspect(Orientation.VERTICAL); + + return ins; + } + + //-----------------// + // getFeatureIndex // + //-----------------// + @Override + public int getFeatureIndex (String label) + { + return LabelsHolder.indices.get(label); + } + + //------------------// + // getFeatureLabels // + //------------------// + @Override + public String[] getFeatureLabels () + { + return LabelsHolder.labels; + } + + //--------// + // length // + //--------// + @Override + public int length () + { + return length; + } + + //~ Inner Classes ---------------------------------------------------------- + //--------------// + // LabelsHolder // + //--------------// + /** + * Descriptive strings for glyph characteristics. + * + * NOTA: Keep in sync method {@link #features} + */ + private static class LabelsHolder + { + //~ Static fields/initializers ----------------------------------------- + + /** Label -> Index */ + public static final Map indices = new HashMap<>(); + + /** Index -> Label */ + public static final String[] labels = new String[length]; + + static { + // We take all the first moments + for (int i = 0; i < momentCount; i++) { + labels[i] = GeometricMoments.getLabel(i); + } + + // We append flags and step position + int i = momentCount; + /* 10 */ labels[i++] = "ledger"; + /* 11 */ labels[i++] = "stemNb"; + /* 12 */ labels[i++] = "aspect"; + + ////* 13 */ labels[i++] = "pitch"; + + // + for (int j = 0; j < labels.length; j++) { + indices.put(labels[j], j); + } + } + + private LabelsHolder () + { + } + } +} diff --git a/src/main/omr/glyph/ShapeEvaluator.java b/src/main/omr/glyph/ShapeEvaluator.java new file mode 100644 index 0000000..3780b62 --- /dev/null +++ b/src/main/omr/glyph/ShapeEvaluator.java @@ -0,0 +1,146 @@ +//----------------------------------------------------------------------------// +// // +// S h a p e E v a l u a t o r // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.glyph; + +import omr.glyph.facets.Glyph; + +import omr.sheet.SystemInfo; + +import omr.util.Predicate; + +import java.util.EnumSet; + +/** + * Interface {@code ShapeEvaluator} defines the features of a glyph + * shape evaluator. + * + * @author Hervé Bitteur + */ +public interface ShapeEvaluator +{ + //~ Static fields/initializers --------------------------------------------- + + /** Empty conditions set */ + public static final EnumSet NO_CONDITIONS = EnumSet.noneOf( + Condition.class); + + //~ Enumerations ----------------------------------------------------------- + /** Conditions for evaluation */ + public static enum Condition + { + //~ Enumeration constant initializers ---------------------------------- + + /** Make sure the shape is not blacklisted by the glyph at hand */ + ALLOWED, + /** Make + * sure all specific checks are successfully passed */ + CHECKED; + + } + + //~ Methods ---------------------------------------------------------------- + /** + * Report the sorted sequence of best evaluation(s) found by the + * evaluator on the provided glyph. + * + * @param glyph the glyph to evaluate + * @param system the system containing the glyph to evaluate + * @param count the desired maximum sequence length + * @param minGrade the minimum evaluation grade to be acceptable + * @param conditions optional conditions, perhaps empty + * @param predicate filter for acceptable shapes, perhaps null + * @return the sequence of evaluations, perhaps empty + */ + Evaluation[] evaluate (Glyph glyph, + SystemInfo system, + int count, + double minGrade, + EnumSet conditions, + Predicate predicate); + + /** + * Report the name of this evaluator. + * + * @return the evaluator declared name + */ + String getName (); + + /** + * Use a threshold on glyph weight, to tell if the provided glyph + * is just {@link Shape#NOISE}, or a real glyph. + * + * @param glyph the glyph to be checked + * @return true if not noise, false otherwise + */ + boolean isBigEnough (Glyph glyph); + + /** + * Report the best evaluation for the provided glyph, above a + * minimum grade value, among the shapes (non checked, but allowed) + * that match the provided predicate. + * + * @param glyph the glyph to evaluate + * @param minGrade the minimum evaluation grade to be acceptable + * @param predicate filter for acceptable shapes, perhaps null + * @return the best acceptable evaluation, or null if none + */ + Evaluation rawVote (Glyph glyph, + double minGrade, + Predicate predicate); + + /** + * Report the best of all evaluations found by the evaluator on the + * provided glyph, under the ALLOWED and CHECKED conditions. + * + * @param glyph the glyph to evaluate + * @param system the system containing the glyph to evaluate + * @param minGrade the minimum evaluation grade to be acceptable + * @return the best acceptable evaluation, or null if none + */ + Evaluation vote (Glyph glyph, + SystemInfo system, + double minGrade); + + /** + * Report the best of all evaluations found by the evaluator on the + * provided glyph, matching the optional conditions and the + * provided predicate. + * + * @param glyph the glyph to evaluate + * @param system the system containing the glyph to evaluate + * @param minGrade the minimum evaluation grade to be acceptable + * @param conditions optional conditions, perhaps empty + * @param predicate filter for acceptable shapes, perhaps null + * @return the best acceptable evaluation, or null if none + */ + Evaluation vote (Glyph glyph, + SystemInfo system, + double minGrade, + EnumSet conditions, + Predicate predicate); + + /** + * Report the best of all evaluations found by the evaluator on the + * provided glyph, under the ALLOWED and CHECKED conditions and + * matching the provided predicate. + * + * @param glyph the glyph to evaluate + * @param system the system containing the glyph to evaluate + * @param minGrade the minimum evaluation grade to be acceptable + * @param predicate filter for acceptable shapes, perhaps null + * @return the best acceptable evaluation, or null if none + */ + Evaluation vote (Glyph glyph, + SystemInfo system, + double minGrade, + Predicate predicate); +} diff --git a/src/main/omr/glyph/ShapeSet.java b/src/main/omr/glyph/ShapeSet.java new file mode 100644 index 0000000..6800efd --- /dev/null +++ b/src/main/omr/glyph/ShapeSet.java @@ -0,0 +1,848 @@ +//----------------------------------------------------------------------------// +// // +// S h a p e S e t // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.glyph; + +import omr.constant.Constant; +import static omr.glyph.Shape.*; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.awt.Color; +import java.awt.event.ActionListener; +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.EnumSet; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import javax.swing.JComponent; +import javax.swing.JMenu; +import javax.swing.JMenuItem; + +/** + * Class {@code ShapeSet} defines a set of related shapes, + * for example the "Rests" set gathers all rest shapes from + * MULTI_REST down to ONE_128TH_REST. + * + *

It handles additional properties over a simple EnumSet, especially + * assigned colors and its automatic insertion in shape menu hierarchy. + * So don't remove any of the ShapeSet's, unless you know what you are doing. + */ +public class ShapeSet +{ + //~ Static fields/initializers --------------------------------------------- + + /** Usual logger utility */ + private static final Logger logger = LoggerFactory.getLogger(ShapeSet.class); + + /** Specific single symbol for part of time signature (such as 4) */ + public static final List PartialTimes = Arrays.asList( + TIME_ZERO, + TIME_ONE, + TIME_TWO, + TIME_THREE, + TIME_FOUR, + TIME_FIVE, + TIME_SIX, + TIME_SEVEN, + TIME_EIGHT, + TIME_NINE, + TIME_TWELVE, + TIME_SIXTEEN); + + /** Specific multi-symbol for full time signature (such as 4/4 or C) */ + public static final EnumSet FullTimes = EnumSet.of( + TIME_FOUR_FOUR, + TIME_TWO_TWO, + TIME_TWO_FOUR, + TIME_THREE_FOUR, + TIME_SIX_EIGHT, + COMMON_TIME, + CUT_TIME, + CUSTOM_TIME); + + /** All sorts of F clefs */ + public static final EnumSet BassClefs = EnumSet.of( + F_CLEF, + F_CLEF_SMALL, + F_CLEF_8VA, + F_CLEF_8VB); + + /** All sorts of G clefs */ + public static final EnumSet TrebleClefs = EnumSet.of( + G_CLEF, + G_CLEF_SMALL, + G_CLEF_8VA, + G_CLEF_8VB); + + /** All sorts of flag sets */ + public static final EnumSet FlagSets = EnumSet.of( + FLAG_1_set, + FLAG_2_set, + FLAG_3_set, + FLAG_4_set, + FLAG_5_set); + + /** All SHARP-based keys */ + public static final EnumSet SharpKeys = EnumSet.of( + KEY_SHARP_1, + KEY_SHARP_2, + KEY_SHARP_3, + KEY_SHARP_4, + KEY_SHARP_5, + KEY_SHARP_6, + KEY_SHARP_7); + + /** All FLAT-based keys */ + public static final EnumSet FlatKeys = EnumSet.of( + KEY_FLAT_1, + KEY_FLAT_2, + KEY_FLAT_3, + KEY_FLAT_4, + KEY_FLAT_5, + KEY_FLAT_6, + KEY_FLAT_7); + + /** All black note heads */ + public static final EnumSet BlackNoteHeads = EnumSet.of( + NOTEHEAD_BLACK, + NOTEHEAD_BLACK_2, + NOTEHEAD_BLACK_3); + + /** All void note heads */ + public static final EnumSet VoidNoteHeads = EnumSet.of( + NOTEHEAD_VOID, + NOTEHEAD_VOID_2, + NOTEHEAD_VOID_3); + + /** + * Predefined instances of ShapeSet. + * Double-check before removing any one of them. + * Nota: Do not use EnumSet.range() since this could lead to subtle errors + * should Shape enum order be modified. Prefer the use of shapesOf() + * which lists precisely all set members. + */ + public static final ShapeSet Accidentals = new ShapeSet( + SHARP, + new Color(0x9933ff), + shapesOf(FLAT, NATURAL, SHARP, DOUBLE_SHARP, DOUBLE_FLAT)); + + public static final ShapeSet Articulations = new ShapeSet( + ARPEGGIATO, + new Color(0xff6699), + shapesOf( + ACCENT, + TENUTO, + STACCATO, + STACCATISSIMO, + STRONG_ACCENT, + ARPEGGIATO)); + + public static final ShapeSet Attributes = new ShapeSet( + PEDAL_MARK, + new Color(0x000000), + shapesOf( + OTTAVA_ALTA, + OTTAVA_BASSA, + PEDAL_MARK, + PEDAL_UP_MARK, + TUPLET_THREE, + TUPLET_SIX)); + + public static final ShapeSet Barlines = new ShapeSet( + LEFT_REPEAT_SIGN, + new Color(0x0000ff), + shapesOf( + PART_DEFINING_BARLINE, + THIN_BARLINE, + THICK_BARLINE, + DOUBLE_BARLINE, + FINAL_BARLINE, + REVERSE_FINAL_BARLINE, + LEFT_REPEAT_SIGN, + RIGHT_REPEAT_SIGN, + BACK_TO_BACK_REPEAT_SIGN)); + + public static final ShapeSet Beams = new ShapeSet( + BEAM, + new Color(0x33ffff), + shapesOf(BEAM, BEAM_2, BEAM_3, BEAM_HOOK)); + + public static final ShapeSet Clefs = new ShapeSet( + G_CLEF, + new Color(0xcc00cc), + shapesOf(TrebleClefs, BassClefs, shapesOf(C_CLEF, PERCUSSION_CLEF))); + + public static final ShapeSet Dynamics = new ShapeSet( + DYNAMICS_F, + new Color(0x009999), + shapesOf( + DYNAMICS_CHAR_M, + DYNAMICS_CHAR_R, + DYNAMICS_CHAR_S, + DYNAMICS_CHAR_Z, + DYNAMICS_F, + DYNAMICS_FF, + DYNAMICS_FFF, + DYNAMICS_FP, + DYNAMICS_FZ, + DYNAMICS_MF, + DYNAMICS_MP, + DYNAMICS_P, + DYNAMICS_PP, + DYNAMICS_PPP, + DYNAMICS_RF, + DYNAMICS_RFZ, + DYNAMICS_SF, + DYNAMICS_SFFZ, + DYNAMICS_SFP, + DYNAMICS_SFPP, + DYNAMICS_SFZ, + CRESCENDO, + DECRESCENDO)); + + public static final ShapeSet Flags = new ShapeSet( + FLAG_1, + new Color(0x99cc00), + shapesOf( + FLAG_1, + FLAG_1_UP, + FLAG_2, + FLAG_2_UP, + FLAG_3, + FLAG_3_UP, + FLAG_4, + FLAG_4_UP, + FLAG_5, + FLAG_5_UP)); + + public static final ShapeSet Keys = new ShapeSet( + KEY_SHARP_3, + new Color(0x00ffff), + shapesOf(FlatKeys, SharpKeys)); + + public static final ShapeSet NoteHeads = new ShapeSet( + NOTEHEAD_BLACK, + new Color(0xff9966), + shapesOf(BlackNoteHeads, VoidNoteHeads)); + + public static final ShapeSet Markers = new ShapeSet( + CODA, + new Color(0x888888), + shapesOf( + DAL_SEGNO, + DA_CAPO, + SEGNO, + CODA, + BREATH_MARK, + CAESURA, + BRACE, + BRACKET)); + + public static final ShapeSet Notes = new ShapeSet( + BREVE, + new Color(0xff66cc), + shapesOf(BREVE, WHOLE_NOTE, WHOLE_NOTE_2, WHOLE_NOTE_3)); + + public static final ShapeSet Ornaments = new ShapeSet( + MORDENT, + new Color(0xcc3300), + shapesOf( + GRACE_NOTE_SLASH, + GRACE_NOTE, + TR, + TURN, + INVERTED_TURN, + TURN_UP, + TURN_SLASH, + MORDENT, + INVERTED_MORDENT)); + + public static final ShapeSet Rests = new ShapeSet( + QUARTER_REST, + new Color(0x99ff66), + shapesOf( + LONG_REST, + BREVE_REST, + WHOLE_REST, + HALF_REST, + QUARTER_REST, + EIGHTH_REST, + ONE_16TH_REST, + ONE_32ND_REST, + ONE_64TH_REST, + ONE_128TH_REST)); + + public static final ShapeSet Times = new ShapeSet( + TIME_FOUR_FOUR, + new Color(0xcc3300), + shapesOf(PartialTimes, FullTimes)); + + public static final ShapeSet Physicals = new ShapeSet( + LEDGER, + new Color(0x9999ff), + shapesOf( + TEXT, + CHARACTER, + CLUTTER, + SLUR, + LEDGER, + STEM, + ENDING, + DOT_set, + REPEAT_DOT, + AUGMENTATION_DOT)); + + // ========================================================================= + // Below are EnumSet instances, used programmatically. + // They do not lead to shape submenus as the ShapeSet instances do. + // ========================================================================= + /** All physical shapes. Here the use of EnumSet.range is OK */ + public static final EnumSet allPhysicalShapes = EnumSet.range( + Shape.values()[0], + LAST_PHYSICAL_SHAPE); + + /** Symbols that can be attached to a stem */ + public static final EnumSet StemSymbols = EnumSet. + copyOf( + shapesOf(NoteHeads.getShapes(), Flags.getShapes(), Beams.getShapes())); + + /** Pedals */ + public static final EnumSet Pedals = EnumSet.of( + PEDAL_MARK, + PEDAL_UP_MARK); + + /** Tuplets */ + public static final EnumSet Tuplets = EnumSet.of( + TUPLET_THREE, + TUPLET_SIX); + + /** All variants of dot */ + public static final EnumSet Dots = EnumSet.of( + DOT_set, + AUGMENTATION_DOT, + STACCATO, + REPEAT_DOT); + + /** Clefs ottava (alta or bassa) */ + public static final EnumSet OttavaClefs = EnumSet.of( + G_CLEF_8VA, + G_CLEF_8VB, + F_CLEF_8VA, + F_CLEF_8VB); + + static { + // Make sure all the shape colors are defined + ShapeSet.defineAllShapeColors(); + + // Debug + ///dumpShapeColors(); + } + + //~ Instance fields -------------------------------------------------------- + // + /** Name of the set */ + private String name; + + /** Underlying shapes */ + private final EnumSet shapes; + + /** Specific sequence of shapes, if any */ + private final List sortedShapes; + + /** The representative shape for this set */ + private final Shape rep; + + /** Assigned color */ + private Color color; + + /** Related color constant */ + private Constant.Color constantColor; + + //~ Constructors ----------------------------------------------------------- + //----------// + // ShapeSet // + //----------// + /** + * Creates a new ShapeSet object from a collection of shapes. + * + * @param rep the representative shape + * @param color the default color assigned + * @param shapes the provided collection of shapes + */ + public ShapeSet (Shape rep, + Color color, + Collection shapes) + { + // The representative shape + this.rep = rep; + + // The default color + this.color = color != null ? color : Color.BLACK; + + // The set of shapes + this.shapes = EnumSet.noneOf(Shape.class); + this.shapes.addAll(shapes); + + // Keep a specific order? + if (shapes instanceof List) { + this.sortedShapes = new ArrayList<>(shapes); + } else { + this.sortedShapes = null; + } + } + + //~ Methods ---------------------------------------------------------------- + // + //-----------------------// + // getPhysicalShapeNames // + //-----------------------// + /** + * Report the names of all the physical shapes. + * + * @return the array of names for shapes up to LAST_PHYSICAL_SHAPE + */ + public static String[] getPhysicalShapeNames () + { + int shapeCount = 1 + LAST_PHYSICAL_SHAPE.ordinal(); + String[] names = new String[shapeCount]; + + for (Shape shape : allPhysicalShapes) { + names[shape.ordinal()] = shape.name(); + } + + return names; + } + + //-----------------// + // addAllShapeSets // + //-----------------// + /** + * Populate the given menu with all ShapeSet instances defined + * in this class. + * + * @param top the JComponent to populate (typically a JMenu or a + * JPopupMenu) + * @param listener the listener for notification of user selection + */ + public static void addAllShapeSets (JComponent top, + ActionListener listener) + { + // All ranges of glyph shapes + for (Field field : ShapeSet.class.getDeclaredFields()) { + if (field.getType() == ShapeSet.class) { + JMenuItem menuItem = new JMenuItem(field.getName()); + ShapeSet set = valueOf(field.getName()); + addColoredItem(top, menuItem, set.getColor()); + menuItem.addActionListener(listener); + } + } + } + + //--------------// + // addAllShapes // + //--------------// + /** + * Populate the given menu with a hierarchy of all shapes, + * organized by defined ShapeSets. + * + * @param top the JComponent to populate (typically a JMenu or a + * JPopupMenu) + * @param listener the listener for notification of user selection + */ + public static void addAllShapes (JComponent top, + ActionListener listener) + { + // All ranges of glyph shapes + for (Field field : ShapeSet.class.getDeclaredFields()) { + if (field.getType() == ShapeSet.class) { + ShapeSet set = ShapeSet.valueOf(field.getName()); + JMenu menu = new JMenu(field.getName()); + + if (set.rep != null) { + menu.setIcon(set.rep.getDecoratedSymbol()); + } + + addColoredItem(top, menu, Color.black); + + // Add menu items for this range + addSetShapes(set, menu, listener); + } + } + } + + //--------------// + // addSetShapes // + //--------------// + /** + * Populate the given menu with a list of all shapes that belong + * to the given ShapeSet. + * + * @param set the set for which shape menu items must be buit + * @param top the JComponent to populate (typically a JMenu or a + * JPopupMenu) + * @param listener the listener for notification of user selection + */ + public static void addSetShapes (ShapeSet set, + JComponent top, + ActionListener listener) + { + // All shapes in the given range + for (Shape shape : set.getSortedShapes()) { + JMenuItem menuItem = new JMenuItem( + shape.toString(), + shape.getDecoratedSymbol()); + addColoredItem(top, menuItem, shape.getColor()); + + menuItem.setToolTipText(shape.getDescription()); + menuItem.addActionListener(listener); + } + } + + //----------// + // contains // + //----------// + /** + * Convenient method to check if encapsulated shapes set does + * contain the provided object. + * + * @param shape the Shape object to check for inclusion + * @return true if contained, false otherwise + */ + public boolean contains (Shape shape) + { + return shapes.contains(shape); + } + + //----------// + // getColor // + //----------// + /** + * Report the color currently assigned to the range, if any. + * + * @return the related color, or null + */ + public Color getColor () + { + return color; + } + + //---------// + // getName // + //---------// + /** + * Report the name of the set. + * + * @return the set name + */ + public String getName () + { + return name; + } + + //--------// + // getRep // + //--------// + /** + * Report the representative shape of the set, if any. + * + * @return the rep shape, or null + */ + public Shape getRep () + { + return rep; + } + + //-------------// + // getShapeSet // + //-------------// + public static ShapeSet getShapeSet (String name) + { + return Sets.map.get(name); + } + + //--------------// + // getShapeSets // + //--------------// + public static List getShapeSets () + { + return Sets.setList; + } + + //----------// + // shapesOf // + //----------// + /** + * Convenient way to build a collection of shapes. + * + * @param col a collection of shapes + * @return a single collection + */ + public static Collection shapesOf (Collection col) + { + Collection shapes = (col instanceof List) + ? new ArrayList() + : EnumSet.noneOf(Shape.class); + + shapes.addAll(col); + + return shapes; + } + + //----------// + // shapesOf // + //----------// + /** + * Convenient way to build a collection of shapes. + * + * @param col1 a first collection of shapes + * @param col2 a second collection of shapes + * @return a single collection + */ + public static Collection shapesOf (Collection col1, + Collection col2) + { + Collection shapes = (col1 instanceof List) + ? new ArrayList() + : EnumSet.noneOf(Shape.class); + + shapes.addAll(col1); + shapes.addAll(col2); + + return shapes; + } + + //----------// + // shapesOf // + //----------// + /** + * Convenient way to build a collection of shapes. + * + * @param col1 a first collection of shapes + * @param col2 a second collection of shapes + * @param col3 a third collection of shapes + * @return a single collection + */ + public static Collection shapesOf (Collection col1, + Collection col2, + Collection col3) + { + Collection shapes = (col1 instanceof List) + ? new ArrayList() + : EnumSet.noneOf(Shape.class); + + shapes.addAll(col1); + shapes.addAll(col2); + shapes.addAll(col3); + + return shapes; + } + + //----------// + // shapesOf // + //----------// + /** + * Convenient way to build a collection of shapes. + * + * @param shapes an array of shapes + * @return a single collection + */ + public static Collection shapesOf (Shape... shapes) + { + return Arrays.asList(shapes); + } + + //---------// + // valueOf // + //---------// + /** + * Retrieve a set knowing its name (just like an enumeration). + * + * @param str the provided set name + * @return the range found, or null otherwise + */ + public static ShapeSet valueOf (String str) + { + return Sets.map.get(str); + } + + //-----------// + // getShapes // + //-----------// + /** + * Exports the set of shapes. + * + * @return the proper enum set + */ + public EnumSet getShapes () + { + return shapes; + } + + //-----------------// + // getSortedShapes // + //-----------------// + /** + * Exports the sorted collection of shapes. + * + * @return the proper enum set + */ + public List getSortedShapes () + { + if (sortedShapes != null) { + return sortedShapes; + } else { + return new ArrayList<>(shapes); + } + } + + //----------// + // setColor // + //----------// + /** + * Assign a display color to the shape set. + * + * @param color the display color + */ + private void setColor (Color color) + { + this.color = color; + } + + //------------------// + // setConstantColor // + //------------------// + /** + * Define a specific color for the set. + * + * @param color the specified color + */ + public void setConstantColor (Color color) + { + constantColor.setValue(color); + setColor(color); + } + + //----------------------// + // defineAllShapeColors // + //----------------------// + /** + * (package private access meant from Shape class) + * Assign a color to every shape, using the color of the containing + * set when no specific color is defined for a shape. + */ + static void defineAllShapeColors () + { + EnumSet colored = EnumSet.noneOf(Shape.class); + + // Define shape colors, using their containing range as default + for (Field field : ShapeSet.class.getDeclaredFields()) { + if (field.getType() == ShapeSet.class) { + try { + ShapeSet set = (ShapeSet) field.get(null); + set.setName(field.getName()); + + // Create shape color for all contained shapes + for (Shape shape : set.shapes) { + shape.createShapeColor(set.getColor()); + colored.add(shape); + } + } catch (IllegalAccessException ex) { + ex.printStackTrace(); + } + } + } + + /** Sets of similar shapes */ + HW_REST_set.createShapeColor(Rests.getColor()); + colored.add(HW_REST_set); + + // Directly assign colors for shapes in no range + EnumSet leftOver = EnumSet.allOf(Shape.class); + leftOver.removeAll(colored); + + for (Shape shape : leftOver) { + shape.createShapeColor(Color.BLACK); + } + } + + //----------------// + // addColoredItem // + //----------------// + private static void addColoredItem (JComponent top, + JMenuItem item, + Color color) + { + if (color != null) { + item.setForeground(color); + } else { + item.setForeground(Color.black); + } + + top.add(item); + } + + //---------// + // setName // + //---------// + private void setName (String name) + { + this.name = name; + + constantColor = new Constant.Color( + getClass().getName(), + name + ".color", + Constant.Color.encodeColor(color), + "Color code for set " + name); + + // Check for a user-modified value + if (!constantColor.isSourceValue()) { + setColor(constantColor.getValue()); + } + } + + //~ Inner Classes ---------------------------------------------------------- + //------// + // Sets // + //------// + /** Build the set map in a lazy way */ + private static class Sets + { + //~ Static fields/initializers ----------------------------------------- + + static final Map map = new HashMap<>(); + + static final List setList = new ArrayList<>(); + + static { + for (Field field : ShapeSet.class.getDeclaredFields()) { + if (field.getType() == ShapeSet.class) { + try { + ShapeSet set = (ShapeSet) field.get(null); + map.put(field.getName(), set); + setList.add(set); + } catch (IllegalAccessException ex) { + ex.printStackTrace(); + } + } + } + } + + private Sets () + { + } + } +} diff --git a/src/main/omr/glyph/SymbolGlyph.java b/src/main/omr/glyph/SymbolGlyph.java new file mode 100644 index 0000000..a143ec7 --- /dev/null +++ b/src/main/omr/glyph/SymbolGlyph.java @@ -0,0 +1,120 @@ +//----------------------------------------------------------------------------// +// // +// S y m b o l G l y p h // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.glyph; + +import omr.glyph.facets.BasicGlyph; +import omr.glyph.facets.Glyph; + +import omr.lag.BasicLag; +import omr.lag.JunctionAllPolicy; +import omr.lag.Lag; +import omr.lag.Section; +import omr.lag.SectionsBuilder; + +import omr.run.Orientation; + +import omr.ui.symbol.MusicFont; +import omr.ui.symbol.ShapeSymbol; +import omr.ui.symbol.SymbolPicture; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.awt.image.BufferedImage; + +/** + * Class {@code SymbolGlyph} is an articial glyph, built from a symbol. + * It is used to generate glyphs for training, when no real glyph (glyph + * retrieved from scanned sheet) is available. + * + * @author Hervé Bitteur + */ +public class SymbolGlyph + extends BasicGlyph +{ + //~ Static fields/initializers --------------------------------------------- + + /** Usual logger utility */ + private static final Logger logger = LoggerFactory.getLogger( + SymbolGlyph.class); + + //~ Instance fields -------------------------------------------------------- + /** The underlying symbol, with generic size */ + private final ShapeSymbol symbol; + + /** The underlying image, properly sized */ + private final BufferedImage image; + + //~ Constructors ----------------------------------------------------------- + //-------------// + // SymbolGlyph // + //-------------// + /** + * Build an (artificial) glyph out of a symbol icon. + * This construction is meant to populate and train on glyph shapes for + * which we have no real instance yet. + * + * @param shape the corresponding shape + * @param symbol the related drawing + * @param interline the related interline scaling value + * @param descriptor additional features, if any + */ + public SymbolGlyph (Shape shape, + ShapeSymbol symbol, + int interline, + SymbolGlyphDescriptor descriptor) + { + super(interline); + this.symbol = symbol; + image = symbol.buildImage(MusicFont.getFont(interline)); + + /** Build a dedicated SymbolPicture */ + SymbolPicture symbolPicture = new SymbolPicture(image); + + /** Build related vertical lag */ + Lag iLag = new BasicLag("iLag", Orientation.VERTICAL); + + new SectionsBuilder(iLag, new JunctionAllPolicy()) // catch all + .createSections("symbol", symbolPicture, /* minRunLength + * => */ 0); + + // Retrieve the whole glyph made of all sections + for (Section section : iLag.getSections()) { + addSection(section, Glyph.Linking.LINK_BACK); + } + + // Glyph features + setShape(shape, Evaluation.MANUAL); + + // Use descriptor if any is provided + if (descriptor != null) { + // Number of connected stems + if (descriptor.getStemNumber() != null) { + setStemNumber(descriptor.getStemNumber()); + } + + // Has a related ledger ? + if (descriptor.isWithLedger() != null) { + setWithLedger(descriptor.isWithLedger()); + } + + // Vertical position wrt staff + if (descriptor.getPitchPosition() != null) { + setPitchPosition(descriptor.getPitchPosition()); + } + } + + if (logger.isDebugEnabled()) { + logger.debug(dumpOf()); + } + } +} diff --git a/src/main/omr/glyph/SymbolGlyphDescriptor.java b/src/main/omr/glyph/SymbolGlyphDescriptor.java new file mode 100644 index 0000000..96d495d --- /dev/null +++ b/src/main/omr/glyph/SymbolGlyphDescriptor.java @@ -0,0 +1,272 @@ +//----------------------------------------------------------------------------// +// // +// S y m b o l G l y p h D e s c r i p t o r // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.glyph; + +import omr.util.PointFacade; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.awt.Point; +import java.io.InputStream; + +import javax.xml.bind.JAXBContext; +import javax.xml.bind.JAXBException; +import javax.xml.bind.Unmarshaller; +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlAttribute; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlRootElement; +import javax.xml.bind.annotation.XmlType; + +/** + * Class {@code SymbolGlyphDescriptor} brings additional information + * to a mere shaped glyph. + *

Such a descriptor contains a simple reference to the related shape, + * whose appearance will be drawn thanks to MusicFont. + * The descriptor can be augmented by informations such as stem number, with + * ledger, pitch position, reference point. + * These informations are thus copied to the {@link SymbolGlyph} instance for + * better training. + * We can have several descriptors from the same shape, which allows + * different values for additional informations (for example, the stem + * number may be 1 or 2 for NOTEHEAD_BLACK shape). + * + * @author Hervé Bitteur + */ +@XmlAccessorType(XmlAccessType.NONE) +@XmlType(name = "", propOrder = { + "xmlRefPoint", "stemNumber", "withLedger", "pitchPosition"}) +@XmlRootElement(name = "symbol") +public class SymbolGlyphDescriptor +{ + //~ Static fields/initializers --------------------------------------------- + + /** Usual logger utility */ + private static final Logger logger = LoggerFactory.getLogger( + SymbolGlyphDescriptor.class); + + /** Un/marshalling context for use with JAXB */ + private static JAXBContext jaxbContext; + + //~ Instance fields -------------------------------------------------------- + /** Image related interline value */ + @XmlAttribute + private Integer interline; + + /** Related name (generally the name of the related shape if any) */ + @XmlAttribute + private String name; + + /** How many stems is it connected to ? */ + @XmlElement(name = "stem-number") + private Integer stemNumber; + + /** Connected to Ledger ? */ + @XmlElement(name = "with-ledger") + private Boolean withLedger; + + /** Pitch position within staff lines */ + @XmlElement(name = "pitch-position") + private Double pitchPosition; + + /** + * Reference point, if any. (Un)Marshalling is done through getXmlRefPoint() + * and setXmlRefPoint(). + */ + private Point refPoint; + + //~ Constructors ----------------------------------------------------------- + //-----------------------// + // SymbolGlyphDescriptor // + //-----------------------// + /** + * No-arg constructor for the XML mapper + */ + private SymbolGlyphDescriptor () + { + } + + //~ Methods ---------------------------------------------------------------- + //-------------------// + // loadFromXmlStream // + //-------------------// + /** + * Load a symbol description from an XML stream. + * + * @param is the input stream + * @return a new SymbolGlyphDescriptor, or null if loading has failed + */ + public static SymbolGlyphDescriptor loadFromXmlStream (InputStream is) + { + try { + return (SymbolGlyphDescriptor) jaxbUnmarshal(is); + } catch (Exception ex) { + ex.printStackTrace(); + + // User already notified + return null; + } + } + + //---------// + // getName // + //---------// + /** + * Report the name (generally the shape name) of the symbol + * + * @return the symbol name + */ + public String getName () + { + return name; + } + + //------------------// + // getPitchPosition // + //------------------// + /** + * Report the pitch position within the staff lines + * + * @return the pitch position + */ + public Double getPitchPosition () + { + return pitchPosition; + } + + //-------------// + // getRefPoint // + //-------------// + /** + * Report the assigned reference point + * + * @return the ref point, which may be null + */ + public Point getRefPoint () + { + return refPoint; + } + + //---------------// + // getStemNumber // + //---------------// + /** + * Report the number of stems this entity is connected to + * + * @return the number of stems + */ + public Integer getStemNumber () + { + return stemNumber; + } + + //--------------// + // isWithLedger // + //--------------// + /** + * Is this entity connected to a ledger + * + * @return true if there is at least one ledger + */ + public Boolean isWithLedger () + { + return withLedger; + } + + //----------// + // toString // + //----------// + @Override + public String toString () + { + StringBuilder sb = new StringBuilder("{"); + sb.append(getClass().getSimpleName()); + + if (name != null) { + sb.append(" name:") + .append(name); + } + + if (interline != null) { + sb.append(" interline:") + .append(interline); + } + + if (stemNumber != null) { + sb.append(" stem-number:") + .append(stemNumber); + } + + if (withLedger != null) { + sb.append(" with-ledger:") + .append(withLedger); + } + + if (pitchPosition != null) { + sb.append(" pitch-position:") + .append(pitchPosition); + } + + sb.append("}"); + + return sb.toString(); + } + + //----------------// + // getJaxbContext // + //----------------// + private static JAXBContext getJaxbContext () + throws JAXBException + { + // Lazy creation + if (jaxbContext == null) { + jaxbContext = JAXBContext.newInstance(SymbolGlyphDescriptor.class); + } + + return jaxbContext; + } + + //---------------// + // jaxbUnmarshal // + //---------------// + private static Object jaxbUnmarshal (InputStream is) + throws JAXBException + { + Unmarshaller um = getJaxbContext() + .createUnmarshaller(); + + return um.unmarshal(is); + } + + //----------------// + // getXmlRefPoint // + //----------------// + private PointFacade getXmlRefPoint () + { + if (refPoint != null) { + return new PointFacade(refPoint); + } else { + return null; + } + } + + //----------------// + // setXmlRefPoint // + //----------------// + @XmlElement(name = "ref-point") + private void setXmlRefPoint (PointFacade xp) + { + refPoint = xp.getPoint(); + } +} diff --git a/src/main/omr/glyph/SymbolsModel.java b/src/main/omr/glyph/SymbolsModel.java new file mode 100644 index 0000000..08d8e4f --- /dev/null +++ b/src/main/omr/glyph/SymbolsModel.java @@ -0,0 +1,306 @@ +//----------------------------------------------------------------------------// +// // +// S y m b o l s M o d e l // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.glyph; + +import omr.glyph.facets.Glyph; + +import omr.score.entity.TimeRational; + +import omr.sheet.Sheet; +import omr.sheet.SystemInfo; + +import omr.step.Steps; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import omr.text.TextBuilder; +import omr.text.TextLine; +import omr.text.TextRole; +import omr.text.TextRoleInfo; +import omr.text.TextWord; + +/** + * Class {@code SymbolsModel} is a GlyphsModel specifically meant for + * symbol glyphs. + * + * @author Hervé Bitteur + */ +public class SymbolsModel + extends GlyphsModel +{ + //~ Static fields/initializers --------------------------------------------- + + /** Usual logger utility */ + private static final Logger logger = LoggerFactory.getLogger(SymbolsModel.class); + + //~ Instance fields -------------------------------------------------------- + /** Standard evaluator */ + private ShapeEvaluator evaluator = GlyphNetwork.getInstance(); + + //~ Constructors ----------------------------------------------------------- + //--------------// + // SymbolsModel // + //--------------// + /** + * Creates a new SymbolsModel object. + * + * @param sheet the related sheet + */ + public SymbolsModel (Sheet sheet) + { + super(sheet, sheet.getNest(), Steps.valueOf(Steps.SYMBOLS)); + } + + //~ Methods ---------------------------------------------------------------- + //------------// + // assignText // + //------------// + /** + * Assign a collection of glyphs as textual element + * + * @param glyphs the collection of glyphs + * @param roleInfo the text role + * @param textContent the ascii content + * @param grade the grade wrt this assignment + */ + public void assignText (Collection glyphs, + TextRoleInfo roleInfo, + String textContent, + double grade) + { + // Do the job + for (Glyph glyph : glyphs) { + SystemInfo system = glyph.getSystem(); + String language = system.getSheet().getPage().getTextParam().getTarget(); + TextBuilder textBuilder = system.getTextBuilder(); + TextWord word = glyph.getTextWord(); + List lines = new ArrayList<>(); + + if (word == null) { + word = TextWord.createManualWord(glyph, textContent); + glyph.setTextWord(language, word); + TextLine line = new TextLine(system, Arrays.asList(word)); + lines = Arrays.asList(line); + lines = textBuilder.recomposeLines(lines); + system.getSentences().remove(line); + system.getSentences().addAll(lines); + } else if (word.getTextLine() != null) { + lines = Arrays.asList(word.getTextLine()); + } + + // Force text role + glyph.setManualRole(roleInfo); + for (TextLine line : lines) { + // For Chord role, we don't spread the role to other words + // but rather trigger a line split + if (roleInfo.role == TextRole.Chord + && line.getWords().size() > 1) { + line.setRole(roleInfo); + List subLines = textBuilder.recomposeLines( + Arrays.asList(line)); + system.getSentences().remove(line); + for (TextLine l : subLines) { + if (!l.getWords().contains(word)) { + l.setRole(null); + } + } + system.getSentences().addAll(subLines); + } else { + line.setRole(roleInfo); + } + } + + // Force text only if it is not empty + if ((textContent != null) && (textContent.length() > 0)) { + glyph.setManualValue(textContent); + } + } + } + + //--------------------// + // assignTimeRational // + //--------------------// + /** + * Assign a time rational value to collection of glyphs + * + * @param glyphs the collection of glyphs + * @param timeRational the time rational value + * @param grade the grade wrt this assignment + */ + public void assignTimeRational (Collection glyphs, + TimeRational timeRational, + double grade) + { + // Do the job + for (Glyph glyph : glyphs) { + glyph.setTimeRational(timeRational); + } + } + + //-------------// + // cancelStems // + //-------------// + /** + * Cancel one or several stems, turning them back to just a set of sections, + * and rebuilding glyphs from their member sections together with the + * neighbouring non-assigned sections + * + * @param stems a list of stems + */ + public void cancelStems (List stems) + { + /** + * To remove a stem, several infos need to be modified : shape from + * STEM to null, result from STEM to null, and the Stem must + * be removed from system list of stems. + * + * The stem glyph must be removed (as well as all other non-recognized + * glyphs that are connected to the former stem) + * + * Then, re-glyph extraction from sections when everything is ready + * (GlyphBuilder). Should work on a micro scale : just the former stem + * and the neighboring (non-assigned) glyphs. + */ + Set impactedSystems = new HashSet<>(); + + for (Glyph stem : stems) { + SystemInfo system = sheet.getSystemOf(stem); + system.removeGlyph(stem); + super.deassignGlyph(stem); + impactedSystems.add(system); + } + + // Extract brand new glyphs from impactedSystems + for (SystemInfo system : impactedSystems) { + system.extractNewGlyphs(); + } + } + + //---------------// + // deassignGlyph // + //---------------// + /** + * Deassign the shape of a glyph. + * This overrides the basic deassignment, in order to delegate the handling + * of some specific shapes. + * + * @param glyph the glyph to deassign + */ + @Override + public void deassignGlyph (Glyph glyph) + { + // Safer + if (glyph.getShape() == null) { + return; + } + + // Processing depends on shape at hand + switch (glyph.getShape()) { + case STEM: + logger.debug("Deassigning a Stem as glyph {}", glyph.getId()); + cancelStems(Collections.singletonList(glyph)); + + break; + + case NOISE: + logger.info("Skipping Noise as glyph {}", glyph.getId()); + + break; + + default: + super.deassignGlyph(glyph); + + break; + } + } + + //---------------// + // segmentGlyphs // + //---------------// + public void segmentGlyphs (Collection glyphs, + boolean isShort) + { + deassignGlyphs(glyphs); + + for (Glyph glyph : new ArrayList<>(glyphs)) { + SystemInfo system = sheet.getSystemOf(glyph); + system.segmentGlyphOnStems(glyph, isShort); + } + } + + //-----------// + // trimSlurs // + //-----------// + public void trimSlurs (Collection glyphs) + { + List slurs = new ArrayList<>(); + + for (Glyph glyph : new ArrayList<>(glyphs)) { + SystemInfo system = sheet.getSystemOf(glyph); + Glyph slur = system.trimSlur(glyph); + + if (slur != null) { + slurs.add(slur); + } + } + + if (!slurs.isEmpty()) { + assignGlyphs(slurs, Shape.SLUR, false, Evaluation.MANUAL); + } + } + + //-------------// + // assignGlyph // + //-------------// + /** + * Assign a Shape to a glyph, inserting the glyph to its containing + * system and lag if it is still transient + * + * @param glyph the glyph to be assigned + * @param shape the assigned shape, which may be null + * @param grade the grade about shape + */ + @Override + protected Glyph assignGlyph (Glyph glyph, + Shape shape, + double grade) + { + if (glyph == null) { + return null; + } + + // Test on glyph weight (noise-like) + // To prevent to assign a non-noise shape to a noise glyph + if ((shape == Shape.NOISE) || evaluator.isBigEnough(glyph)) { + // Force a recomputation of glyph parameters + // (since environment may have changed since the time they + // have been computed) + SystemInfo system = sheet.getSystemOf(glyph); + + if (system != null) { + system.computeGlyphFeatures(glyph); + + return super.assignGlyph(glyph, shape, grade); + } + } + + return glyph; + } +} diff --git a/src/main/omr/glyph/VirtualGlyph.java b/src/main/omr/glyph/VirtualGlyph.java new file mode 100644 index 0000000..969def2 --- /dev/null +++ b/src/main/omr/glyph/VirtualGlyph.java @@ -0,0 +1,98 @@ +//----------------------------------------------------------------------------// +// // +// V i r t u a l G l y p h // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.glyph; + +import omr.lag.Section; + +import omr.math.GeoUtil; + +import omr.ui.symbol.Symbols; + +import java.awt.Color; +import java.awt.Point; +import java.util.Collection; + +/** + * Class {@code VirtualGlyph} is an artificial glyph specifically + * build from a MusicFont-based symbol, to carry a shape and features + * just like a standard glyph would. + * + * @author Hervé Bitteur + */ +public class VirtualGlyph + extends SymbolGlyph +{ + //~ Constructors ----------------------------------------------------------- + + //--------------// + // VirtualGlyph // + //--------------// + /** + * Create a new VirtualGlyph object + * + * @param shape the assigned shape + * @param interline the related interline scaling value + * @param center where the glyph area center will be located + */ + public VirtualGlyph (Shape shape, + int interline, + Point center) + { + // Build a glyph of proper size + super(shape, Symbols.getSymbol(shape), interline, null); + + // Translation from generic center to actual center + translate(GeoUtil.vectorOf(getAreaCenter(), center)); + } + + //~ Methods ---------------------------------------------------------------- + //----------// + // colorize // + //----------// + @Override + public void colorize (Color color) + { + // Nothing to colorize + } + + //----------// + // colorize // + //----------// + @Override + public void colorize (Collection

sections, + Color color) + { + } + + //----------// + // isActive // + //----------// + /** + * By definition a virtual glyph is always active + * + * @return true + */ + @Override + public boolean isActive () + { + return true; + } + + //-----------// + // isVirtual // + //-----------// + @Override + public boolean isVirtual () + { + return true; + } +} diff --git a/src/main/omr/glyph/doc-files/GlyphEvaluator.uxf b/src/main/omr/glyph/doc-files/GlyphEvaluator.uxf new file mode 100644 index 0000000..4c9ded0 --- /dev/null +++ b/src/main/omr/glyph/doc-files/GlyphEvaluator.uxf @@ -0,0 +1,225 @@ + + + +fontsize=14 + + // My own comments + + 10 + + com.umlet.element.Class + + 290 + 620 + 140 + 50 + + *LinearEvaluator* +bg=#ffffaa +-- + + + + + com.umlet.element.Relation + + 210 + 600 + 100 + 50 + + lt=<- + 80;30;30;30 + + + com.umlet.element.Class + + 290 + 480 + 140 + 70 + + *GlyphChecker* +bg=#ffffaa +-- +annotate() +relax() + + + + + com.umlet.element.Relation + + 180 + 460 + 130 + 50 + + lt=<- + 110;30;30;30 + + + com.umlet.element.Class + + 10 + 10 + 140 + 150 + + <<interface>> +*/GlyphEvaluator/* +-- +Condition +-- +getName() +isBigEnough() +evaluate() +vote() +rawVote() + + + + + + com.umlet.element.Class + + 10 + 210 + 140 + 160 + + <<interface>> +*/EvaluationEngine/* +-- +StartingMode +Monitor +-- +dump() +train() +stop() +marshal() + + + + + + + com.umlet.element.Class + + 10 + 420 + 200 + 120 + + <<abstract>> +*/AbstractEvaluationEngine/* +bg=#ffffaa +-- +#unmarshal() +#getFileName() +-getBackupName() +/#getRawEvaluations()/ + + + + + com.umlet.element.Relation + + 40 + 340 + 50 + 100 + + lt=<<. + 30;30;30;80 + + + com.umlet.element.Class + + 10 + 670 + 150 + 50 + + *GlyphNetwork* +bg=#ffffaa +-- +#getRawEvaluations() + + + + + com.umlet.element.Class + + 80 + 580 + 160 + 70 + + *GlyphRegression* +bg=#ffffaa +-- +#getRawEvaluations() +constraintsMatched() + + + + + com.umlet.element.Relation + + 20 + 510 + 50 + 180 + + lt=<<- + 30;30;30;160 + + + com.umlet.element.Relation + + 110 + 510 + 50 + 90 + + lt=<<- + 30;30;30;70 + + + com.umlet.element.Class + + 290 + 690 + 140 + 50 + + *NeuralNetwork* +bg=#ffffaa +-- + + + + + com.umlet.element.Relation + + 130 + 670 + 180 + 50 + + lt=<- + 160;30;30;30 + + + com.umlet.element.Relation + + 40 + 130 + 50 + 100 + + lt=<<- + 30;30;30;80 + + diff --git a/src/main/omr/glyph/doc-files/Glyphs.uxf b/src/main/omr/glyph/doc-files/Glyphs.uxf new file mode 100644 index 0000000..c954125 --- /dev/null +++ b/src/main/omr/glyph/doc-files/Glyphs.uxf @@ -0,0 +1,22 @@ + +fontsize=14 + + // My own comments +com.umlet.element.base.Class34020100180Glyph +-- +result +shape +com.umlet.element.base.Relation10016026040lt=<<<-> +m1= allGlyphs20;20;240;20com.umlet.element.base.Relation10013026040lt=<<<-> +m1= originals20;20;240;20com.umlet.element.base.Relation10010026040lt=<<<- +m1= activeGlyphs20;20;240;20com.umlet.element.base.Relation10025026040lt=<<<<- +m1= sections20;20;240;20com.umlet.element.base.Relation2603010040lt=<<<->20;20;80;20com.umlet.element.base.Relation1003010040lt=<<<<- +m1=1..n20;20;80;20com.umlet.element.base.Class1804010030SystemInfo +com.umlet.element.base.Class34026010030Section +com.umlet.element.base.Class20110100180GlyphLag +com.umlet.element.base.Class202010070Sheet +com.umlet.element.base.Relation50704060lt=<-20;40;20;20com.umlet.element.base.Relation33018060100lt=<<<-> +m1=members30;20;30;80com.umlet.element.base.Relation40018040100lt=-> +m2=0..120;80;20;20com.umlet.element.base.Relation4201013070lt=<<<- +m1=0..n parts +m2=0..1 partOf20;20;110;20;110;50;20;50 \ No newline at end of file diff --git a/src/main/omr/glyph/facets/BasicAdministration.java b/src/main/omr/glyph/facets/BasicAdministration.java new file mode 100644 index 0000000..90b8f4d --- /dev/null +++ b/src/main/omr/glyph/facets/BasicAdministration.java @@ -0,0 +1,174 @@ +//----------------------------------------------------------------------------// +// // +// B a s i c A d m i n i s t r a t i o n // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.glyph.facets; + +import omr.glyph.Nest; + +/** + * Class {@code BasicAdministration} is a basic implementation of glyph + * administration facet + * + * @author Hervé Bitteur + */ +class BasicAdministration + extends BasicFacet + implements GlyphAdministration +{ + //~ Instance fields -------------------------------------------------------- + + /** The containing glyph nest. */ + protected Nest nest; + + /** Glyph instance identifier. (Unique in the containing nest) */ + protected int id; + + /** Flag to remember processing has been done. */ + private boolean processed = false; + + /** VIP flag. */ + protected boolean vip; + + /** Related id string (prebuilt once for all) */ + protected String idString; + + //~ Constructors ----------------------------------------------------------- + //---------------------// + // BasicAdministration // + //---------------------// + /** + * Create a new BasicAdministration object + * + * @param glyph our glyph + */ + public BasicAdministration (Glyph glyph) + { + super(glyph); + } + + //~ Methods ---------------------------------------------------------------- + //--------// + // dumpOf // + //--------// + @Override + public String dumpOf () + { + StringBuilder sb = new StringBuilder(); + + sb.append(String.format("Glyph: %s@%s%n", + glyph.getClass().getName(), + Integer.toHexString(glyph.hashCode()))); + sb.append(String.format(" id=%d%n", getId())); + sb.append(String.format(" nest=%s%n", getNest())); + + return sb.toString(); + } + + //-------// + // getId // + //-------// + @Override + public int getId () + { + return id; + } + + //---------// + // getNest // + //---------// + @Override + public Nest getNest () + { + return nest; + } + + //----------// + // idString // + //----------// + @Override + public final String idString () + { + return idString; + } + + //-------------// + // isProcessed // + //-------------// + @Override + public boolean isProcessed () + { + return processed; + } + + //-------------// + // isTransient // + //-------------// + @Override + public boolean isTransient () + { + return nest == null; + } + + //-------// + // isVip // + //-------// + @Override + public boolean isVip () + { + return vip; + } + + //-----------// + // isVirtual // + //-----------// + @Override + public boolean isVirtual () + { + return false; + } + + //-------// + // setId // + //-------// + @Override + public void setId (int id) + { + this.id = id; + idString = "glyph#" + id; + } + + //---------// + // setNest // + //---------// + @Override + public void setNest (Nest nest) + { + this.nest = nest; + } + + //--------------// + // setProcessed // + //--------------// + @Override + public void setProcessed (boolean processed) + { + this.processed = processed; + } + + //--------// + // setVip // + //--------// + @Override + public void setVip () + { + vip = true; + } +} diff --git a/src/main/omr/glyph/facets/BasicAlignment.java b/src/main/omr/glyph/facets/BasicAlignment.java new file mode 100644 index 0000000..a718f14 --- /dev/null +++ b/src/main/omr/glyph/facets/BasicAlignment.java @@ -0,0 +1,577 @@ +//----------------------------------------------------------------------------// +// // +// B a s i c A l i g n m e n t // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.glyph.facets; + +import omr.constant.ConstantSet; + +import omr.glyph.Glyphs; + +import omr.lag.Section; + +import omr.math.Barycenter; +import omr.math.BasicLine; +import omr.math.Line; + +import omr.run.Orientation; +import static omr.run.Orientation.*; +import omr.run.Run; + +import omr.sheet.Scale; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.awt.Graphics2D; +import java.awt.Rectangle; +import java.awt.geom.Point2D; + +/** + * Class {@code BasicAlignment} implements a basic handling of + * Alignment facet + * + * @author Hervé Bitteur + */ +public class BasicAlignment + extends BasicFacet + implements GlyphAlignment +{ + //~ Static fields/initializers --------------------------------------------- + + /** Specific application parameters */ + private static final Constants constants = new Constants(); + + /** Usual logger utility */ + private static final Logger logger = LoggerFactory.getLogger( + BasicAlignment.class); + + //~ Instance fields -------------------------------------------------------- + /** Best (curved or straight) line equation */ + protected Line line; + + /** Absolute slope of the line wrt abscissa axis */ + protected Double slope; + + /** Absolute beginning point */ + protected Point2D startPoint; + + /** Absolute ending point */ + protected Point2D stopPoint; + + //~ Constructors ----------------------------------------------------------- + /** + * Create a new BasicAlignment object + * + * @param glyph our glyph + */ + public BasicAlignment (Glyph glyph) + { + super(glyph); + } + + //~ Methods ---------------------------------------------------------------- + //-----------// + // getAspect // + //-----------// + @Override + public final double getAspect (Orientation orientation) + { + Rectangle box = glyph.getBounds(); + + if (orientation == HORIZONTAL) { + return (double) box.width / (double) box.height; + } else { + return (double) box.height / (double) box.width; + } + } + + //---------------// + // getFirstStuck // + //---------------// + @Override + public int getFirstStuck () + { + int stuck = 0; + + for (Section section : glyph.getMembers()) { + Run sectionRun = section.getFirstRun(); + + for (Section sct : section.getSources()) { + if (!sct.isGlyphMember() || (sct.getGlyph() != glyph)) { + stuck += sectionRun.getCommonLength(sct.getLastRun()); + } + } + } + + return stuck; + } + + //------------------// + // getIntPositionAt // + //------------------// + public int getIntPositionAt (double coord, + Orientation orientation) + { + return (int) Math.rint(getPositionAt(coord, orientation)); + } + + //------------------// + // getInvertedSlope // + //------------------// + @Override + public double getInvertedSlope () + { + checkLine(); + + return (stopPoint.getX() - startPoint.getX()) / (stopPoint.getY() + - startPoint.getY()); + } + + //--------------// + // getLastStuck // + //--------------// + @Override + public int getLastStuck () + { + int stuck = 0; + + for (Section section : glyph.getMembers()) { + Run sectionRun = section.getLastRun(); + + for (Section sct : section.getTargets()) { + if (!sct.isGlyphMember() || (sct.getGlyph() != glyph)) { + stuck += sectionRun.getCommonLength(sct.getFirstRun()); + } + } + } + + return stuck; + } + + //-----------// + // getLength // + //-----------// + @Override + public final int getLength (Orientation orientation) + { + Rectangle box = glyph.getBounds(); + + if (orientation == HORIZONTAL) { + return box.width; + } else { + return box.height; + } + } + + //---------// + // getLine // + //---------// + @Override + public Line getLine () + { + checkLine(); + + return line; + } + + //-----------------// + // getMeanDistance // + //-----------------// + @Override + public double getMeanDistance () + { + if (line == null) { + computeLine(); + } + + return line.getMeanDistance(); + } + + //------------------// + // getMeanThickness // + //------------------// + @Override + public double getMeanThickness (Orientation orientation) + { + return (double) glyph.getWeight() / getLength(orientation); + } + + //-----------// + // getMidPos // + //-----------// + @Override + public int getMidPos (Orientation orientation) + { + if (orientation == VERTICAL) { + return (int) Math.rint( + (getStartPoint(orientation) + .getX() + getStopPoint(orientation) + .getX()) / 2.0); + } else { + return (int) Math.rint( + (getStartPoint(orientation) + .getY() + getStopPoint(orientation) + .getY()) / 2.0); + } + } + + //---------------// + // getPositionAt // + //---------------// + @Override + public double getPositionAt (double coord, + Orientation orientation) + { + if (orientation == HORIZONTAL) { + return getLine() + .yAtX(coord); + } else { + return getLine() + .xAtY(coord); + } + } + + //----------------------// + // getRectangleCentroid // + //----------------------// + /** + * Report the absolute centroid of all the glyph pixels found in the + * provided absolute ROI + * + * @param absRoi the desired absolute region of interest + * @return the absolute barycenter of the pixels found + */ + @Override + public Point2D getRectangleCentroid (Rectangle absRoi) + { + Barycenter barycenter = new Barycenter(); + + for (Section section : glyph.getMembers()) { + section.cumulate(barycenter, absRoi); + } + + if (barycenter.getWeight() != 0) { + return new Point2D.Double(barycenter.getX(), barycenter.getY()); + } else { + return null; + } + } + + //---------------// + // getStartPoint // + //---------------// + @Override + public Point2D getStartPoint (Orientation orientation) + { + checkLine(); + + if (orientation == Orientation.HORIZONTAL) { + // Use left side + if (startPoint.getX() <= stopPoint.getX()) { + return startPoint; + } else { + return stopPoint; + } + } else { + // Use top side + if (startPoint.getY() <= stopPoint.getY()) { + return startPoint; + } else { + return stopPoint; + } + } + } + + //--------------// + // getStopPoint // + //--------------// + @Override + public Point2D getStopPoint (Orientation orientation) + { + checkLine(); + + if (orientation == Orientation.HORIZONTAL) { + // Use right side + if (stopPoint.getX() >= startPoint.getX()) { + return stopPoint; + } else { + return startPoint; + } + } else { + // Use bottom side + if (stopPoint.getY() >= startPoint.getY()) { + return stopPoint; + } else { + return startPoint; + } + } + } + + //--------------// + // getThickness // + //--------------// + @Override + public final int getThickness (Orientation orientation) + { + Rectangle box = glyph.getBounds(); + + if (orientation == HORIZONTAL) { + return box.height; + } else { + return box.width; + } + } + + //---------------// + // getProbeWidth // + //---------------// + /** + * Report the width of the window used to determine filament ordinate + * + * @return the scale-independent probe width + */ + public static Scale.Fraction getProbeWidth () + { + return constants.probeWidth; + } + + //--------// + // dumpOf // + //--------// + /** + * Report glyph internal data + */ + @Override + public String dumpOf () + { + StringBuilder sb = new StringBuilder(); + + if (startPoint != null) { + sb.append(String.format(" start=%s%n", startPoint)); + } + + if (stopPoint != null) { + sb.append(String.format(" stop=%s%n", stopPoint)); + } + + sb.append(String.format(" line=%s%n", getLine())); + + return sb.toString(); + } + + //----------// + // getSlope // + //----------// + @Override + public double getSlope () + { + if (slope == null) { + checkLine(); + + slope = (stopPoint.getY() - startPoint.getY()) / (stopPoint.getX() + - startPoint.getX()); + } + + return slope; + } + + //----------------// + // getThicknessAt // + //----------------// + @Override + public double getThicknessAt (double coord, + Orientation orientation) + { + return Glyphs.getThicknessAt(coord, orientation, glyph); + } + + //-----------------// + // invalidateCache // + //-----------------// + @Override + public void invalidateCache () + { + line = null; + slope = null; + startPoint = stopPoint = null; + } + + //------------// + // renderLine // + //------------// + @Override + public void renderLine (Graphics2D g) + { + if (!glyph.getBounds() + .intersects(g.getClipBounds())) { + return; + } + + getLine(); // To make sure the line has been computed + + g.drawLine( + (int) Math.rint(startPoint.getX()), + (int) Math.rint(startPoint.getY()), + (int) Math.rint(stopPoint.getX()), + (int) Math.rint(stopPoint.getY())); + } + + //-----------------// + // setEndingPoints // + //-----------------// + @Override + public void setEndingPoints (Point2D pStart, + Point2D pStop) + { + glyph.invalidateCache(); + this.startPoint = pStart; + this.stopPoint = pStop; + + computeLine(); + + // Enlarge contour box if needed + Rectangle box = glyph.getBounds(); + box.add(pStart); + box.add(pStop); + glyph.setContourBox(box); + } + + //-----------// + // checkLine // + //-----------// + /** + * Make sure an approximating line is available + */ + protected final void checkLine () + { + if (line == null) { + computeLine(); + } + } + + //-------------// + // computeLine // + //-------------// + protected void computeLine () + { + line = new BasicLine(); + + for (Section section : glyph.getMembers()) { + line.includeLine(section.getAbsoluteLine()); + } + + Rectangle box = glyph.getBounds(); + + // We have a problem if glyph is just 1 pixel: no computable slope! + if (glyph.getWeight() <= 1) { + startPoint = stopPoint = box.getLocation(); + slope = 0d; // Why not? we just need a value. + + return; + } + + slope = line.getSlope(); + + double top = box.y; + double bot = (box.y + box.height) - 1; + double left = box.x; + double right = (box.x + box.width) - 1; + + if (isRatherVertical()) { + // Use line intersections with top & bottom box sides + startPoint = new Point2D.Double(line.xAtY(top), top); + stopPoint = new Point2D.Double(line.xAtY(bot), bot); + + if (!line.isVertical()) { + Point2D pLeft = new Point2D.Double(left, line.yAtX(left)); + Point2D pRight = new Point2D.Double(right, line.yAtX(right)); + + if (line.getInvertedSlope() > 0) { + if (pLeft.getY() > startPoint.getY()) { + startPoint = pLeft; + } + + if (pRight.getY() < stopPoint.getY()) { + stopPoint = pRight; + } + } else { + if (pRight.getY() > startPoint.getY()) { + startPoint = pRight; + } + + if (pLeft.getY() < stopPoint.getY()) { + stopPoint = pLeft; + } + } + } + } else { + // Use line intersections with left & right box sides + startPoint = new Point2D.Double(left, line.yAtX(left)); + stopPoint = new Point2D.Double(right, line.yAtX(right)); + + if (!line.isHorizontal()) { + Point2D pTop = new Point2D.Double(line.xAtY(top), top); + Point2D pBot = new Point2D.Double(line.xAtY(bot), bot); + + if (slope > 0) { + if (pTop.getX() > startPoint.getX()) { + startPoint = pTop; + } + + if (pBot.getX() < stopPoint.getX()) { + stopPoint = pBot; + } + } else { + if (pBot.getX() > startPoint.getX()) { + startPoint = pBot; + } + + if (pTop.getX() < stopPoint.getX()) { + stopPoint = pTop; + } + } + } + } + } + + //------------------// + // isRatherVertical // + //------------------// + /** + * Report whether the angle of the approximating line is outside + * the range [-PI/4 - +PI/4]. + * + * @return true if rather vertical, false for rather horizontal + */ + private boolean isRatherVertical () + { + if (slope == null) { + computeLine(); + } + + return Math.abs(slope) > (Math.PI / 4); + } + + //~ Inner Classes ---------------------------------------------------------- + //-----------// + // Constants // + //-----------// + private static final class Constants + extends ConstantSet + { + //~ Instance fields ---------------------------------------------------- + + final Scale.Fraction probeWidth = new Scale.Fraction( + 0.5, + "Width of probing window to retrieve Glyph ordinate"); + + } +} diff --git a/src/main/omr/glyph/facets/BasicComposition.java b/src/main/omr/glyph/facets/BasicComposition.java new file mode 100644 index 0000000..b2e9ae7 --- /dev/null +++ b/src/main/omr/glyph/facets/BasicComposition.java @@ -0,0 +1,337 @@ +//----------------------------------------------------------------------------// +// // +// B a s i c C o m p o s i t i o n // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.glyph.facets; + +import omr.check.Result; +import omr.check.SuccessResult; + +import omr.glyph.Shape; + +import omr.lag.Section; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import omr.sheet.SystemInfo; + +import java.awt.Point; +import java.util.Collections; +import java.util.SortedSet; +import java.util.TreeSet; + +/** + * Class {@code BasicComposition} implements the composition facet of + * a glyph made of sections (and possibly of other sub-glyphs). + * These member sections may belong to different lags. + * + * @author Hervé Bitteur + */ +class BasicComposition + extends BasicFacet + implements GlyphComposition +{ + + /** Usual logger utility */ + private static final Logger logger = LoggerFactory.getLogger(BasicComposition.class); + + /** + * Sections that compose this glyph. + * The collection is kept sorted on natural Section order (abscissa then + * ordinate, even with mixed section orientations). + */ + private final SortedSet
members = new TreeSet<>(); + + /** Unmodifiable view on members */ + private final SortedSet
unmodifiableMembers = Collections.unmodifiableSortedSet(members); + + /** Link to the compound, if any, this one is a part of */ + private Glyph partOf; + + /** Result of analysis wrt this glyph */ + private Result result; + + //------------------// + // BasicComposition // + //------------------// + /** + * Create a new BasicComposition object. + * + * @param glyph our glyph + */ + public BasicComposition (Glyph glyph) + { + super(glyph); + } + + //------------// + // addSection // + //------------// + @Override + public void addSection (Section section, + Linking link) + { + if (section == null) { + throw new IllegalArgumentException("Cannot add a null section"); + } + +// if (!glyph.isTransient()) { +// logger.error("Adding section to registered glyph"); +// } + + // Nota: We must include the section in the glyph members before + // linking back the section to the containing glyph. + // Otherwise, there is a risk of using the glyph box (which depends on + // its member sections) before the section is in the glyph members. + // This phenomenum was sometimes observed when using parallelism. + + /** First, update glyph data */ + members.add(section); + + /** Second, update section data, if so desired */ + if (link == Linking.LINK_BACK) { + section.setGlyph(glyph); + } + + glyph.invalidateCache(); + } + + //-------------// + // addSections // + //-------------// + public void addSections (Glyph other, + Linking linkSections) + { + // Update glyph info in other sections + for (Section section : other.getMembers()) { + addSection(section, linkSections); + } + } + + //-----------------// + // containsSection // + //-----------------// + @Override + public boolean containsSection (int id) + { + for (Section section : glyph.getMembers()) { + if (section.getId() == id) { + return true; + } + } + + return false; + } + + //-------------// + // cutSections // + //-------------// + @Override + public void cutSections () + { + for (Section section : glyph.getMembers()) { + if (section.getGlyph() == glyph) { + section.setGlyph(null); + } + } + } + + //--------// + // dumpOf // + //--------// + @Override + public String dumpOf () + { + StringBuilder sb = new StringBuilder(); + + sb.append(String.format(" members=%s%n", members)); + + if (partOf != null) { + sb.append(String.format(" partOf=%s%n", partOf)); + } + + if (result != null) { + sb.append(String.format(" result=%s%n", result)); + } + + return sb.toString(); + } + + //----------------// + // getAlienSystem // + //----------------// + @Override + public SystemInfo getAlienSystem (SystemInfo system) + { + // Direct members + for (Section section : glyph.getMembers()) { + if (section.getSystem() != system) { + return section.getSystem(); + } + } + + // No other system found + return null; + } + + //-------------// + // getAncestor // + //-------------// + @Override + public Glyph getAncestor () + { + Glyph g = this.glyph; + + while (g.getPartOf() != null) { + g = g.getPartOf(); + } + + return g; + } + + //-----------------// + // getFirstSection // + //-----------------// + @Override + public Section getFirstSection () + { + return glyph.getMembers().first(); + } + + //------------// + // getMembers // + //------------// + @Override + public SortedSet
getMembers () + { + return unmodifiableMembers; + } + + //-----------// + // getPartOf // + //-----------// + @Override + public Glyph getPartOf () + { + return partOf; + } + + //-----------// + // getResult // + //-----------// + @Override + public Result getResult () + { + return result; + } + + //----------// + // isActive // + //----------// + @Override + public boolean isActive () + { + if (glyph.getShape() == Shape.GLYPH_PART) { + return false; + } + + for (Section section : glyph.getMembers()) { + if (section.getGlyph() != glyph) { + return false; + } + } + + return true; + } + + //--------------// + // isSuccessful // + //--------------// + @Override + public boolean isSuccessful () + { + return result instanceof SuccessResult; + } + + //-----------------// + // linkAllSections // + //-----------------// + @Override + public void linkAllSections () + { + for (Section section : glyph.getMembers()) { + section.setGlyph(glyph); + } + } + + //---------------// + // removeSection // + //---------------// + @Override + public boolean removeSection (Section section, + Linking link) + { + if (link == Linking.LINK_BACK) { + section.setGlyph(null); + } + + boolean bool = members.remove(section); + glyph.invalidateCache(); + + return bool; + } + + //-----------// + // setPartOf // + //-----------// + @Override + public void setPartOf (Glyph compound) + { + partOf = compound; + } + + //-----------// + // setResult // + //-----------// + @Override + public void setResult (Result result) + { + this.result = result; + } + + //---------------// + // stealSections // + //---------------// + @Override + public void stealSections (Glyph that) + { + for (Section section : that.getMembers()) { + addSection(section, Linking.LINK_BACK); + } + + that.setPartOf(glyph); + } + + //-----------// + // translate // + //-----------// + /** + * Apply the provided translation vector to all composing sections. + * + * @param vector the provided translation vector + */ + public void translate (Point vector) + { + for (Section section : glyph.getMembers()) { + section.translate(vector); + } + } +} diff --git a/src/main/omr/glyph/facets/BasicDisplay.java b/src/main/omr/glyph/facets/BasicDisplay.java new file mode 100644 index 0000000..ef0856c --- /dev/null +++ b/src/main/omr/glyph/facets/BasicDisplay.java @@ -0,0 +1,224 @@ +//----------------------------------------------------------------------------// +// // +// B a s i c D i s p l a y // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.glyph.facets; + +import omr.glyph.ui.AttachmentHolder; +import omr.glyph.ui.BasicAttachmentHolder; + +import omr.lag.BasicSection; +import omr.lag.Section; + +import omr.ui.Colors; + +import java.awt.Color; +import java.awt.Graphics2D; +import java.awt.Rectangle; +import java.awt.image.BufferedImage; +import java.util.Collection; +import java.util.Collections; +import java.util.Map; + +/** + * Class {@code BasicDisplay} is the basic implementation of a display + * facet. + * + * @author Hervé Bitteur + */ +class BasicDisplay + extends BasicFacet + implements GlyphDisplay +{ + //~ Instance fields -------------------------------------------------------- + + /** Potential attachments, lazily allocated */ + protected AttachmentHolder attachments; + + //~ Constructors ----------------------------------------------------------- + //--------------// + // BasicDisplay // + //--------------// + /** + * Create a new BasicDisplay object. + * + * @param glyph our glyph + */ + public BasicDisplay (Glyph glyph) + { + super(glyph); + } + + //~ Methods ---------------------------------------------------------------- + //---------------// + // addAttachment // + //---------------// + @Override + public void addAttachment (String id, + java.awt.Shape attachment) + { + if (attachment != null) { + if (attachments == null) { + attachments = new BasicAttachmentHolder(); + } + + attachments.addAttachment(id, attachment); + } + } + + //----------// + // colorize // + //----------// + @Override + public void colorize (Color color) + { + colorize(glyph.getMembers(), color); + } + + //----------// + // colorize // + //----------// + @Override + public void colorize (Collection
sections, + Color color) + { + for (Section section : sections) { + section.setColor(color); + } + } + + //--------------// + // asciiDrawing // + //--------------// + @Override + public String asciiDrawing () + { + StringBuilder sb = new StringBuilder(); + + sb.append(String.format("%s%n", glyph)); + + // Determine the bounding box + Rectangle box = glyph.getBounds(); + + if (box == null) { + return sb.toString(); // Safer + } + + // Allocate the drawing table + char[][] table = BasicSection.allocateTable(box); + + // Register each section in turn + for (Section section : glyph.getMembers()) { + section.fillTable(table, box); + } + + // Draw the result + sb.append(BasicSection.drawingOfTable(table, box)); + + return sb.toString(); + } + + //--------// + // dumpOf // + //--------// + @Override + public String dumpOf () + { + StringBuilder sb = new StringBuilder(); + + if (attachments != null) { + sb.append(String.format(" attachments=%s%n", getAttachments())); + } + + return sb.toString(); + } + + //----------------// + // getAttachments // + //----------------// + @Override + public Map getAttachments () + { + if (attachments != null) { + return attachments.getAttachments(); + } else { + return Collections.emptyMap(); + } + } + + //----------// + // getColor // + //----------// + @Override + public Color getColor () + { + if (glyph.getShape() == null) { + return Colors.SHAPE_UNKNOWN; + } else { + return glyph.getShape() + .getColor(); + } + } + + //----------// + // getImage // + //----------// + @Override + public BufferedImage getImage () + { + // Determine the bounding box + Rectangle box = glyph.getBounds(); + BufferedImage image = new BufferedImage( + box.width, + box.height, + BufferedImage.TYPE_BYTE_GRAY); + + for (Section section : glyph.getMembers()) { + section.fillImage(image, box); + } + + return image; + } + + //------------// + // recolorize // + //------------// + @Override + public void recolorize () + { + for (Section section : glyph.getMembers()) { + section.resetColor(); + } + } + + //-------------------// + // removeAttachments // + //-------------------// + @Override + public int removeAttachments (String prefix) + { + if (attachments != null) { + return attachments.removeAttachments(prefix); + } else { + return 0; + } + } + + //-------------------// + // renderAttachments // + //-------------------// + @Override + public void renderAttachments (Graphics2D g) + { + if (attachments != null) { + attachments.renderAttachments(g); + } + } +} diff --git a/src/main/omr/glyph/facets/BasicEnvironment.java b/src/main/omr/glyph/facets/BasicEnvironment.java new file mode 100644 index 0000000..c602d36 --- /dev/null +++ b/src/main/omr/glyph/facets/BasicEnvironment.java @@ -0,0 +1,368 @@ +//----------------------------------------------------------------------------// +// // +// B a s i c E n v i r o n m e n t // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.glyph.facets; + +import omr.lag.Lag; +import omr.lag.Section; + +import omr.run.Run; + +import omr.sheet.SystemInfo; + +import omr.util.HorizontalSide; +import omr.util.Predicate; + +import java.awt.Rectangle; +import java.util.HashSet; +import java.util.Set; + +/** + * Class {@code BasicEnvironment} is the basic implementation of an + * environment facet. + * + * @author Hervé Bitteur + */ +class BasicEnvironment + extends BasicFacet + implements GlyphEnvironment +{ + //~ Instance fields -------------------------------------------------------- + + /** Position with respect to nearest staff. Key references are : 0 for + * middle line (B), -2 for top line (F) and +2 for bottom line (E) */ + private double pitchPosition; + + /** Number of stems it is connected to (0, 1, 2) */ + private int stemNumber; + + /** Stem attached on left if any */ + private Glyph leftStem; + + /** Stem attached on right if any */ + private Glyph rightStem; + + /** Is there a ledger nearby ? */ + private boolean withLedger; + + //~ Constructors ----------------------------------------------------------- + //------------------// + // BasicEnvironment // + //------------------// + /** + * Create a new BasicEnvironment object + * + * @param glyph our glyph + */ + public BasicEnvironment (Glyph glyph) + { + super(glyph); + } + + //~ Methods ---------------------------------------------------------------- + //---------------------// + // copyStemInformation // + //---------------------// + @Override + public void copyStemInformation (Glyph other) + { + for (HorizontalSide side : HorizontalSide.values()) { + setStem(other.getStem(side), side); + } + + setStemNumber(other.getStemNumber()); + } + + //--------// + // dumpOf // + //--------// + @Override + public String dumpOf () + { + StringBuilder sb = new StringBuilder(); + + if (stemNumber > 0) { + sb.append(String.format(" stemNumber=%s%n", stemNumber)); + } + + if (leftStem != null) { + sb.append(String.format(" leftStem=%s%n", leftStem)); + } + + if (rightStem != null) { + sb.append(String.format(" rightStem=%s%n", rightStem)); + } + + sb.append(String.format(" pitchPosition=%s%n", getPitchPosition())); + + if (withLedger) { + sb.append(String.format(" withLedger%n")); + } + + return sb.toString(); + } + + //--------------------// + // getAlienPixelsFrom // + //--------------------// + @Override + public int getAlienPixelsFrom (Lag lag, + Rectangle absRoi, + Predicate
predicate) + { + // Use lag orientation + final Rectangle oRoi = lag.getOrientation() + .oriented(absRoi); + final int posMin = oRoi.y; + final int posMax = (oRoi.y + oRoi.height) - 1; + int count = 0; + + for (Section section : lag.lookupIntersectedSections(absRoi)) { + // Exclude sections that are part of the glyph + if (section.getGlyph() == glyph) { + continue; + } + + // Additional section predicate, if any + if ((predicate != null) && !predicate.check(section)) { + continue; + } + + int pos = section.getFirstPos() - 1; + + for (Run run : section.getRuns()) { + pos++; + + if (pos > posMax) { + break; + } + + if (pos < posMin) { + continue; + } + + int coordMin = Math.max(oRoi.x, run.getStart()); + int coordMax = Math.min( + (oRoi.x + oRoi.width) - 1, + run.getStop()); + + if (coordMax >= coordMin) { + count += (coordMax - coordMin + 1); + } + } + } + + return count; + } + + //-----------------------// + // getConnectedNeighbors // + //-----------------------// + @Override + public Set getConnectedNeighbors () + { + Set
sections = glyph.getMembers(); + + // Retrieve sections connected to this glyph + Set
connectedSections = new HashSet<>(); + + for (Section section : sections) { + for (Section s : section.getSources()) { + if (!sections.contains(s)) { + connectedSections.add(s); + } + } + + for (Section s : section.getTargets()) { + if (!sections.contains(s)) { + connectedSections.add(s); + } + } + + for (Section s : section.getOppositeSections()) { + if (!sections.contains(s)) { + connectedSections.add(s); + } + } + } + + // Retrieve their containing glyphs + Set connectedGlyphs = new HashSet<>(); + + for (Section s : connectedSections) { + Glyph g = s.getGlyph(); + + if (g != null) { + connectedGlyphs.add(g); + } + } + + return connectedGlyphs; + } + + //--------------// + // getFirstStem // + //--------------// + @Override + public Glyph getFirstStem () + { + for (HorizontalSide side : HorizontalSide.values()) { + Glyph stem = getStem(side); + + if (stem != null) { + return stem; + } + } + + return null; + } + + //------------------// + // getPitchPosition // + //------------------// + @Override + public double getPitchPosition () + { + return pitchPosition; + } + + //---------// + // getStem // + //---------// + @Override + public Glyph getStem (HorizontalSide side) + { + if (side == HorizontalSide.LEFT) { + return leftStem; + } else { + return rightStem; + } + } + + //---------------// + // getStemNumber // + //---------------// + @Override + public int getStemNumber () + { + return stemNumber; + } + + //-----------------// + // getSymbolsAfter // + //-----------------// + @Override + public void getSymbolsAfter (Predicate predicate, + Set goods, + Set bads) + { + for (Section section : glyph.getMembers()) { + for (Section sct : section.getTargets()) { + if (sct.isGlyphMember()) { + Glyph other = sct.getGlyph(); + + if (other != glyph) { + if (predicate.check(other)) { + goods.add(other); + } else { + bads.add(other); + } + } + } + } + } + } + + //------------------// + // getSymbolsBefore // + //------------------// + @Override + public void getSymbolsBefore (Predicate predicate, + Set goods, + Set bads) + { + for (Section section : glyph.getMembers()) { + for (Section sct : section.getSources()) { + if (sct.isGlyphMember()) { + Glyph other = sct.getGlyph(); + + if (other != glyph) { + if (predicate.check(other)) { + goods.add(other); + } else { + bads.add(other); + } + } + } + } + } + } + + //-----------// + // getSystem // + //-----------// + @Override + public SystemInfo getSystem () + { + return glyph.getFirstSection() + .getSystem(); + } + + //--------------// + // isWithLedger // + //--------------// + @Override + public boolean isWithLedger () + { + return withLedger; + } + + //------------------// + // setPitchPosition // + //------------------// + @Override + public void setPitchPosition (double pitchPosition) + { + this.pitchPosition = pitchPosition; + } + + //---------// + // setStem // + //---------// + @Override + public void setStem (Glyph stem, + HorizontalSide side) + { + if (side == HorizontalSide.LEFT) { + leftStem = stem; + } else { + rightStem = stem; + } + } + + //---------------// + // setStemNumber // + //---------------// + @Override + public void setStemNumber (int stemNumber) + { + this.stemNumber = stemNumber; + } + + //---------------// + // setWithLedger // + //---------------// + @Override + public void setWithLedger (boolean withLedger) + { + this.withLedger = withLedger; + } +} diff --git a/src/main/omr/glyph/facets/BasicFacet.java b/src/main/omr/glyph/facets/BasicFacet.java new file mode 100644 index 0000000..e449350 --- /dev/null +++ b/src/main/omr/glyph/facets/BasicFacet.java @@ -0,0 +1,64 @@ +//----------------------------------------------------------------------------// +// // +// B a s i c F a c e t // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.glyph.facets; + +import omr.util.Navigable; + +/** + * Class {@code BasicFacet} is the root for implementation on any + * glyph facet. + * + * @author Hervé Bitteur + */ +public class BasicFacet + implements GlyphFacet +{ + //~ Instance fields -------------------------------------------------------- + + /** Our glyph */ + @Navigable(false) + protected final Glyph glyph; + + //~ Constructors ----------------------------------------------------------- + //------------// + // BasicFacet // + //------------// + /** + * Create a new BasicFacet object + * + * @param glyph the glyph this facet describes + */ + public BasicFacet (Glyph glyph) + { + this.glyph = glyph; + } + + //~ Methods ---------------------------------------------------------------- + //--------// + // dumpOf // + //--------// + @Override + public String dumpOf () + { + // Empty by default + return ""; + } + + //-----------------// + // invalidateCache // + //-----------------// + @Override + public void invalidateCache () + { + // void by default + } +} diff --git a/src/main/omr/glyph/facets/BasicGeometry.java b/src/main/omr/glyph/facets/BasicGeometry.java new file mode 100644 index 0000000..b7a17b0 --- /dev/null +++ b/src/main/omr/glyph/facets/BasicGeometry.java @@ -0,0 +1,470 @@ +//----------------------------------------------------------------------------// +// // +// B a s i c G e o m e t r y // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.glyph.facets; + +import omr.glyph.GlyphSignature; +import omr.glyph.Shape; + +import omr.lag.Section; + +import omr.math.Circle; +import omr.math.PointsCollector; + +import omr.moments.ARTMoments; +import omr.moments.BasicARTExtractor; +import omr.moments.BasicARTMoments; +import omr.moments.GeometricMoments; + +import omr.ui.symbol.ShapeSymbol; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.awt.Point; +import java.awt.Rectangle; + +/** + * Class {@code BasicGeometry} is the basic implementation of the + * geometry facet. + * + * @author Hervé Bitteur + */ +class BasicGeometry + extends BasicFacet + implements GlyphGeometry +{ + //~ Static fields/initializers --------------------------------------------- + + /** Usual logger utility */ + private static final Logger logger = LoggerFactory.getLogger( + BasicGeometry.class); + + //~ Instance fields -------------------------------------------------------- + /** Interline of the containing staff (or sheet) */ + private final int interline; + + /** Total weight of this glyph */ + private Integer weight; + + /** Computed ART Moments of this glyph */ + private ARTMoments artMoments; + + /** Computed geometric Moments of this glyph */ + private GeometricMoments geometricMoments; + + /** Mass center coordinates */ + private Point centroid; + + /** Box center coordinates */ + private Point center; + + /** Absolute contour box */ + private Rectangle bounds; + + /** Current signature */ + private GlyphSignature signature; + + /** Signature used for registration */ + private GlyphSignature registeredSignature; + + /** Approximating circle, if any */ + private Circle circle; + + //~ Constructors ----------------------------------------------------------- + //---------------// + // BasicGeometry // + //---------------// + /** + * Create a new BasicGeometry object. + * + * @param glyph our glyph + * @param interline the interline scaling value + */ + public BasicGeometry (Glyph glyph, + int interline) + { + super(glyph); + this.interline = interline; + } + + //~ Methods ---------------------------------------------------------------- + //--------// + // dumpOf // + //--------// + @Override + public String dumpOf () + { + StringBuilder sb = new StringBuilder(); + + sb.append(String.format(" bounds=%s%n", getBounds())); + sb.append(String.format(" centroid=%s%n", getCentroid())); + sb.append(String.format(" interline=%s%n", getInterline())); + sb.append(String.format(" location=%s%n", getLocation())); + sb.append(String.format(" geoMoments=%s%n", getGeometricMoments())); + sb.append(String.format(" artMoments=%s%n", getARTMoments())); + sb.append(String.format(" weight=%s%n", getWeight())); + + if (circle != null) { + sb.append(String.format(" circle=%s%n", circle)); + } + + return sb.toString(); + } + + //---------------// + // getARTMoments // + //---------------// + @Override + public ARTMoments getARTMoments () + { + if (artMoments == null) { + computeARTMoments(); + } + + return artMoments; + } + + //---------------// + // getAreaCenter // + //---------------// + @Override + public Point getAreaCenter () + { + if (center == null) { + Rectangle box = glyph.getBounds(); + center = new Point( + box.x + (box.width / 2), + box.y + (box.height / 2)); + } + + return center; + } + + //-----------// + // getBounds // + //-----------// + @Override + public Rectangle getBounds () + { + if (bounds == null) { + Rectangle box = null; + + for (Section section : glyph.getMembers()) { + if (box == null) { + box = section.getBounds(); + } else { + box.add(section.getBounds()); + } + } + + bounds = box; + } + + if (bounds != null) { + return new Rectangle(bounds); // Return a COPY + } else { + return null; + } + } + + //-------------// + // getCentroid // + //-------------// + @Override + public Point getCentroid () + { + if (centroid == null) { + centroid = getGeometricMoments() + .getCentroid(); + } + + return centroid; + } + + //-----------// + // getCircle // + //-----------// + @Override + public Circle getCircle () + { + return circle; + } + + //------------// + // getDensity // + //------------// + @Override + public double getDensity () + { + Rectangle rect = getBounds(); + int surface = (rect.width + 1) * (rect.height + 1); + + return (double) getWeight() / (double) surface; + } + + //---------------------// + // getGeometricMoments // + //---------------------// + @Override + public GeometricMoments getGeometricMoments () + { + if (geometricMoments == null) { + computeGeometricMoments(); + } + + return geometricMoments; + } + + //--------------// + // getInterline // + //--------------// + @Override + public int getInterline () + { + return interline; + } + + //-------------// + // getLocation // + //-------------// + @Override + public Point getLocation () + { + Shape shape = glyph.getShape(); + + // No shape: use area center + if (shape == null) { + return getAreaCenter(); + } + + // Text shape: use specific reference + if (shape.isText()) { + Point loc = glyph.getTextLocation(); + + if (loc != null) { + return loc; + } + } + + // Other shape: check with the related symbol if any + ShapeSymbol symbol = shape.getSymbol(); + + if (symbol != null) { + return symbol.getRefPoint(getBounds()); + } + + // Default: use area center + return getAreaCenter(); + } + + //---------------------// + // getNormalizedHeight // + //---------------------// + @Override + public double getNormalizedHeight () + { + return getGeometricMoments() + .getHeight(); + } + + //---------------------// + // getNormalizedWeight // + //---------------------// + @Override + public double getNormalizedWeight () + { + return getGeometricMoments() + .getWeight(); + } + + //--------------------// + // getNormalizedWidth // + //--------------------// + @Override + public double getNormalizedWidth () + { + return getGeometricMoments() + .getWidth(); + } + + //--------------------// + // getPointsCollector // + //--------------------// + @Override + public PointsCollector getPointsCollector () + { + // Cumulate point from member sections + PointsCollector collector = new PointsCollector(null, getWeight()); + + // Append all points, whatever section orientation + for (Section section : glyph.getMembers()) { + section.cumulate(collector); + } + + return collector; + } + + //------------------------// + // getRegisteredSignature // + //------------------------// + @Override + public GlyphSignature getRegisteredSignature () + { + return registeredSignature; + } + + //--------------// + // getSignature // + //--------------// + @Override + public GlyphSignature getSignature () + { + if (signature == null) { + signature = new GlyphSignature(glyph); + } + + return signature; + } + + //-----------// + // getWeight // + //-----------// + @Override + public int getWeight () + { + if (weight == null) { + weight = 0; + + for (Section section : glyph.getMembers()) { + weight += section.getWeight(); + } + } + + return weight; + } + + //------------// + // intersects // + //------------// + @Override + public boolean intersects (Rectangle rectangle) + { + // First make a rough test + if (rectangle.intersects(glyph.getBounds())) { + // Then make sure at least one section intersects the rectangle + for (Section section : glyph.getMembers()) { + if (rectangle.intersects(section.getBounds())) { + return true; + } + } + } + + return false; + } + + //-----------------// + // invalidateCache // + //-----------------// + @Override + public void invalidateCache () + { + signature = null; + center = null; + centroid = null; + bounds = null; + artMoments = null; + geometricMoments = null; + weight = null; + circle = null; + } + + //-----------// + // setCircle // + //-----------// + @Override + public void setCircle (Circle circle) + { + this.circle = circle; + } + + //---------------// + // setContourBox // + //---------------// + @Override + public void setContourBox (Rectangle contourBox) + { + this.bounds = contourBox; + } + + //------------------------// + // setRegisteredSignature // + //------------------------// + @Override + public void setRegisteredSignature (GlyphSignature sig) + { + registeredSignature = sig; + } + + //-----------// + // translate // + //-----------// + @Override + public void translate (Point vector) + { + for (Section section : glyph.getMembers()) { + section.translate(vector); + } + + glyph.invalidateCache(); + } + + //-------------------// + // computeARTMoments // + //-------------------// + private void computeARTMoments () + { + // Retrieve glyph foreground points + PointsCollector collector = glyph.getPointsCollector(); + + // Then compute the ART moments with this collector + artMoments = new BasicARTMoments(); + + BasicARTExtractor extractor = new BasicARTExtractor(); + extractor.setDescriptor(artMoments); + extractor.extract( + collector.getXValues(), + collector.getYValues(), + collector.getSize()); + } + + //-------------------------// + // computeGeometricMoments // + //-------------------------// + private void computeGeometricMoments () + { + // Retrieve glyph foreground points + PointsCollector collector = glyph.getPointsCollector(); + + // Then compute the geometric moments with this collector + try { + geometricMoments = new GeometricMoments( + collector.getXValues(), + collector.getYValues(), + collector.getSize(), + getInterline()); + } catch (Exception ex) { + logger.warn( + "Glyph #{} Cannot compute moments with unit set to 0", + glyph.getId()); + } + } +} diff --git a/src/main/omr/glyph/facets/BasicGlyph.java b/src/main/omr/glyph/facets/BasicGlyph.java new file mode 100644 index 0000000..1f68464 --- /dev/null +++ b/src/main/omr/glyph/facets/BasicGlyph.java @@ -0,0 +1,1103 @@ +//----------------------------------------------------------------------------// +// // +// B a s i c G l y p h // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.glyph.facets; + +import omr.check.Result; + +import omr.glyph.Evaluation; +import omr.glyph.GlyphSignature; +import omr.glyph.Nest; +import omr.glyph.Shape; + +import omr.lag.Lag; +import omr.lag.Section; + +import omr.math.Circle; +import omr.math.Line; +import omr.math.PointsCollector; + +import omr.moments.ARTMoments; +import omr.moments.GeometricMoments; + +import omr.run.Orientation; + +import omr.score.entity.PartNode; +import omr.score.entity.TimeRational; + +import omr.sheet.SystemInfo; + +import omr.text.BasicContent; +import omr.text.TextRoleInfo; +import omr.text.TextWord; + +import omr.util.HorizontalSide; +import omr.util.Predicate; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.awt.Color; +import java.awt.Graphics2D; +import java.awt.Point; +import java.awt.Rectangle; +import java.awt.geom.Point2D; +import java.awt.image.BufferedImage; +import java.lang.reflect.Constructor; +import java.util.Collection; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Set; +import java.util.SortedSet; + +/** + * Class {@code BasicGlyph} is the basic Glyph implementation. + * + *

From an implementation point of view, this {@code BasicGlyph} is just a + * shell around specialized Glyph facets, and most of the methods are simply + * using delegation to the proper facet. + * + * @author Hervé Bitteur + */ +public class BasicGlyph + implements Glyph +{ + //~ Static fields/initializers --------------------------------------------- + + /** Usual logger utility */ + private static final Logger logger = LoggerFactory.getLogger(BasicGlyph.class); + + //~ Instance fields -------------------------------------------------------- + /** All needed facets */ + final GlyphAdministration administration; + + final GlyphComposition composition; + + final GlyphDisplay display; + + final GlyphEnvironment environment; + + final GlyphGeometry geometry; + + final GlyphRecognition recognition; + + final GlyphTranslation translation; + + final GlyphAlignment alignment; + + // The content facet is not final to allow lazy allocation + protected GlyphContent content; + + // Set all facets + final Set facets = new LinkedHashSet<>(); + + //~ Constructors ----------------------------------------------------------- + //------------// + // BasicGlyph // + //------------// + /** + * Create a new BasicGlyph object. + * + * @param interline the scaling interline value + */ + public BasicGlyph (int interline) + { + addFacet(administration = new BasicAdministration(this)); + addFacet(composition = new BasicComposition(this)); + addFacet(display = new BasicDisplay(this)); + addFacet(environment = new BasicEnvironment(this)); + addFacet(geometry = new BasicGeometry(this, interline)); + addFacet(recognition = new BasicRecognition(this)); + addFacet(translation = new BasicTranslation(this)); + addFacet(alignment = new BasicAlignment(this)); + } + + //------------// + // BasicGlyph // + //------------// + /** + * Create a new BasicGlyph object from a GlyphValue instance + * (typically unmarshalled from XML). + * + * @param value the GlyphValue "builder" object + */ + public BasicGlyph (GlyphValue value) + { + this(value.interline); + + setId(value.id); + setShape(value.shape); + setStemNumber(value.stemNumber); + setWithLedger(value.withLedger); + setPitchPosition(value.pitchPosition); + + for (Section section : value.members) { + addSection(section, Linking.NO_LINK_BACK); + } + } + + //------------// + // BasicGlyph // + //------------// + /** + * Create a glyph with a specific alignment class. + * + * @param interline the scaling information + * @param alignmentClass the specific alignment class + */ + protected BasicGlyph (int interline, + Class alignmentClass) + { + addFacet(administration = new BasicAdministration(this)); + addFacet(composition = new BasicComposition(this)); + addFacet(display = new BasicDisplay(this)); + addFacet(environment = new BasicEnvironment(this)); + addFacet(geometry = new BasicGeometry(this, interline)); + addFacet(recognition = new BasicRecognition(this)); + addFacet(translation = new BasicTranslation(this)); + + GlyphAlignment theAlignment = null; + + try { + Constructor constructor = alignmentClass.getConstructor( + new Class[]{Glyph.class}); + theAlignment = (GlyphAlignment) constructor.newInstance( + new Object[]{this}); + } catch (Exception ex) { + logger.error( + "Cannot instantiate BasicGlyph with {} ex:{}", + alignmentClass, + ex); + } + + addFacet(alignment = theAlignment); + } + + //~ Methods ---------------------------------------------------------------- + @Override + public void addAttachment (String id, + java.awt.Shape attachment) + { + display.addAttachment(id, attachment); + } + + @Override + public void addSection (Section section, + Linking link) + { + composition.addSection(section, link); + } + + @Override + public void addTranslation (PartNode entity) + { + translation.addTranslation(entity); + } + + @Override + public void allowShape (Shape shape) + { + recognition.allowShape(shape); + } + + @Override + public void clearTranslations () + { + translation.clearTranslations(); + } + + @Override + public void colorize (Collection

sections, + Color color) + { + display.colorize(sections, color); + } + + @Override + public void colorize (Color color) + { + display.colorize(color); + } + + @Override + public boolean containsSection (int id) + { + return composition.containsSection(id); + } + + @Override + public void copyStemInformation (Glyph glyph) + { + environment.copyStemInformation(glyph); + } + + @Override + public void cutSections () + { + composition.cutSections(); + } + + @Override + public String asciiDrawing () + { + return display.asciiDrawing(); + } + + //--------// + // dumpOf // + //--------// + @Override + public String dumpOf () + { + StringBuilder sb = new StringBuilder(); + + for (GlyphFacet facet : facets) { + sb.append(facet.dumpOf()); + } + + return sb.toString(); + } + + @Override + public void forbidShape (Shape shape) + { + recognition.forbidShape(shape); + } + + @Override + public ARTMoments getARTMoments () + { + return geometry.getARTMoments(); + } + + @Override + public int getAlienPixelsFrom (Lag lag, + Rectangle absRoi, + Predicate
predicate) + { + return environment.getAlienPixelsFrom(lag, absRoi, predicate); + } + + @Override + public SystemInfo getAlienSystem (SystemInfo system) + { + return composition.getAlienSystem(system); + } + + @Override + public Glyph getAncestor () + { + return composition.getAncestor(); + } + + @Override + public Point getAreaCenter () + { + return geometry.getAreaCenter(); + } + + @Override + public double getAspect (Orientation orientation) + { + return alignment.getAspect(orientation); + } + + @Override + public Map getAttachments () + { + return display.getAttachments(); + } + + @Override + public Rectangle getBounds () + { + return geometry.getBounds(); + } + + @Override + public Point getCentroid () + { + return geometry.getCentroid(); + } + + @Override + public Circle getCircle () + { + return geometry.getCircle(); + } + + @Override + public Color getColor () + { + return display.getColor(); + } + + @Override + public Set getConnectedNeighbors () + { + return environment.getConnectedNeighbors(); + } + + @Override + public double getDensity () + { + return geometry.getDensity(); + } + + @Override + public Evaluation getEvaluation () + { + return recognition.getEvaluation(); + } + + @Override + public Section getFirstSection () + { + return composition.getFirstSection(); + } + + @Override + public Glyph getFirstStem () + { + return environment.getFirstStem(); + } + + @Override + public int getFirstStuck () + { + return alignment.getFirstStuck(); + } + + @Override + public GeometricMoments getGeometricMoments () + { + return geometry.getGeometricMoments(); + } + + @Override + public double getGrade () + { + return recognition.getGrade(); + } + + @Override + public int getId () + { + return administration.getId(); + } + + @Override + public BufferedImage getImage () + { + return display.getImage(); + } + + @Override + public int getInterline () + { + return geometry.getInterline(); + } + + @Override + public double getInvertedSlope () + { + return alignment.getInvertedSlope(); + } + + @Override + public int getLastStuck () + { + return alignment.getLastStuck(); + } + + @Override + public int getLength (Orientation orientation) + { + return alignment.getLength(orientation); + } + + @Override + public Line getLine () + { + return alignment.getLine(); + } + + @Override + public Point getLocation () + { + return geometry.getLocation(); + } + + @Override + public TextRoleInfo getManualRole () + { + return getContent() + .getManualRole(); + } + + @Override + public String getManualValue () + { + return getContent() + .getManualValue(); + } + + @Override + public double getMeanDistance () + { + return alignment.getMeanDistance(); + } + + @Override + public double getMeanThickness (Orientation orientation) + { + return alignment.getMeanThickness(orientation); + } + + @Override + public SortedSet
getMembers () + { + return composition.getMembers(); + } + + @Override + public int getMidPos (Orientation orientation) + { + return alignment.getMidPos(orientation); + } + + @Override + public Nest getNest () + { + return administration.getNest(); + } + + @Override + public double getNormalizedHeight () + { + return geometry.getNormalizedHeight(); + } + + @Override + public double getNormalizedWeight () + { + return geometry.getNormalizedWeight(); + } + + @Override + public double getNormalizedWidth () + { + return geometry.getNormalizedWidth(); + } + + @Override + public String getOcrLanguage () + { + return getContent() + .getOcrLanguage(); + } + + @Override + public Glyph getPartOf () + { + return composition.getPartOf(); + } + + @Override + public double getPitchPosition () + { + return environment.getPitchPosition(); + } + + @Override + public PointsCollector getPointsCollector () + { + return geometry.getPointsCollector(); + } + + @Override + public double getPositionAt (double coord, + Orientation orientation) + { + return alignment.getPositionAt(coord, orientation); + } + + @Override + public Point2D getRectangleCentroid (Rectangle absRoi) + { + return alignment.getRectangleCentroid(absRoi); + } + + @Override + public GlyphSignature getRegisteredSignature () + { + return geometry.getRegisteredSignature(); + } + + @Override + public Result getResult () + { + return composition.getResult(); + } + + @Override + public Shape getShape () + { + return recognition.getShape(); + } + + @Override + public GlyphSignature getSignature () + { + return geometry.getSignature(); + } + + @Override + public double getSlope () + { + return alignment.getSlope(); + } + + @Override + public Point2D getStartPoint (Orientation orientation) + { + return alignment.getStartPoint(orientation); + } + + @Override + public Glyph getStem (HorizontalSide side) + { + return environment.getStem(side); + } + + @Override + public int getStemNumber () + { + return environment.getStemNumber(); + } + + @Override + public Point2D getStopPoint (Orientation orientation) + { + return alignment.getStopPoint(orientation); + } + + @Override + public void getSymbolsAfter (Predicate predicate, + Set goods, + Set bads) + { + environment.getSymbolsAfter(predicate, goods, bads); + } + + @Override + public void getSymbolsBefore (Predicate predicate, + Set goods, + Set bads) + { + environment.getSymbolsBefore(predicate, goods, bads); + } + + @Override + public SystemInfo getSystem () + { + return environment.getSystem(); + } + + @Override + public Point getTextLocation () + { + return getContent() + .getTextLocation(); + } + + @Override + public TextRoleInfo getTextRole () + { + return getContent() + .getTextRole(); + } + + @Override + public String getTextValue () + { + return getContent() + .getTextValue(); + } + + @Override + public TextWord getTextWord () + { + return getContent() + .getTextWord(); + } + + @Override + public int getThickness (Orientation orientation) + { + return alignment.getThickness(orientation); + } + + @Override + public double getThicknessAt (double coord, + Orientation orientation) + { + return alignment.getThicknessAt(coord, orientation); + } + + @Override + public TimeRational getTimeRational () + { + return recognition.getTimeRational(); + } + + @Override + public Collection getTranslations () + { + return translation.getTranslations(); + } + + @Override + public int getWeight () + { + return geometry.getWeight(); + } + + @Override + public String idString () + { + return administration.idString(); + } + + @Override + public boolean intersects (Rectangle rectangle) + { + return geometry.intersects(rectangle); + } + + //-----------------// + // invalidateCache // + //-----------------// + @Override + public void invalidateCache () + { + // Invalidate all allocated facets + for (GlyphFacet facet : facets) { + facet.invalidateCache(); + } + } + + @Override + public boolean isActive () + { + return composition.isActive(); + } + + @Override + public boolean isBar () + { + return recognition.isBar(); + } + + @Override + public boolean isClef () + { + return recognition.isClef(); + } + + @Override + public boolean isKnown () + { + return recognition.isKnown(); + } + + @Override + public boolean isManualShape () + { + return recognition.isManualShape(); + } + + @Override + public boolean isProcessed () + { + return administration.isProcessed(); + } + + @Override + public boolean isShapeForbidden (Shape shape) + { + return recognition.isShapeForbidden(shape); + } + + @Override + public boolean isStem () + { + return recognition.isStem(); + } + + @Override + public boolean isSuccessful () + { + return composition.isSuccessful(); + } + + @Override + public boolean isText () + { + return recognition.isText(); + } + + @Override + public boolean isTransient () + { + return administration.isTransient(); + } + + @Override + public boolean isTranslated () + { + return translation.isTranslated(); + } + + @Override + public boolean isVip () + { + return administration.isVip(); + } + + @Override + public boolean isVirtual () + { + return administration.isVirtual(); + } + + @Override + public boolean isWellKnown () + { + return recognition.isWellKnown(); + } + + @Override + public boolean isWithLedger () + { + return environment.isWithLedger(); + } + + @Override + public void linkAllSections () + { + composition.linkAllSections(); + } + + @Override + public void recolorize () + { + display.recolorize(); + } + + @Override + public int removeAttachments (String prefix) + { + return display.removeAttachments(prefix); + } + + @Override + public boolean removeSection (Section section, + Linking link) + { + return composition.removeSection(section, link); + } + + @Override + public void renderAttachments (Graphics2D g) + { + display.renderAttachments(g); + } + + @Override + public void renderLine (Graphics2D g) + { + alignment.renderLine(g); + } + + @Override + public void resetEvaluation () + { + recognition.resetEvaluation(); + } + + @Override + public void setCircle (Circle circle) + { + geometry.setCircle(circle); + } + + @Override + public void setContourBox (Rectangle contourBox) + { + geometry.setContourBox(contourBox); + } + + @Override + public void setEndingPoints (Point2D pStart, + Point2D pStop) + { + alignment.setEndingPoints(pStart, pStop); + } + + @Override + public void setEvaluation (Evaluation evaluation) + { + recognition.setEvaluation(evaluation); + } + + @Override + public void setId (int id) + { + administration.setId(id); + } + + @Override + public void setManualRole (TextRoleInfo manualRole) + { + getContent() + .setManualRole(manualRole); + } + + @Override + public void setManualValue (String manualValue) + { + getContent() + .setManualValue(manualValue); + } + + @Override + public void setNest (Nest nest) + { + administration.setNest(nest); + } + + @Override + public void setPartOf (Glyph compound) + { + composition.setPartOf(compound); + } + + @Override + public void setPitchPosition (double pitchPosition) + { + environment.setPitchPosition(pitchPosition); + } + + @Override + public void setProcessed (boolean processed) + { + administration.setProcessed(processed); + } + + @Override + public void setRegisteredSignature (GlyphSignature sig) + { + geometry.setRegisteredSignature(sig); + } + + @Override + public void setResult (Result result) + { + composition.setResult(result); + } + + @Override + public void setShape (Shape shape, + double grade) + { + recognition.setShape(shape, grade); + } + + @Override + public void setShape (Shape shape) + { + recognition.setShape(shape); + } + + @Override + public void setStem (Glyph stem, + HorizontalSide side) + { + environment.setStem(stem, side); + } + + @Override + public void setStemNumber (int stemNumber) + { + environment.setStemNumber(stemNumber); + } + + @Override + public void setTextWord (String ocrLanguage, + TextWord textWord) + { + getContent() + .setTextWord(ocrLanguage, textWord); + } + + @Override + public void setTimeRational (TimeRational timeRational) + { + recognition.setTimeRational(timeRational); + } + + @Override + public void setTranslation (PartNode entity) + { + translation.setTranslation(entity); + } + + @Override + public void setVip () + { + administration.setVip(); + } + + @Override + public void setWithLedger (boolean withLedger) + { + environment.setWithLedger(withLedger); + } + + @Override + public void stealSections (Glyph that) + { + composition.stealSections(that); + } + + //----------// + // toString // + //----------// + @Override + public String toString () + { + StringBuilder sb = new StringBuilder(); + + sb.append("{") + .append(getClass().getSimpleName()) + .append("#") + .append(this.getId()); + + sb.append(internalsString()); + + sb.append("}"); + + return sb.toString(); + } + + @Override + public void translate (Point vector) + { + geometry.translate(vector); + } + + //--------------// + // getAlignment // + //--------------// + protected GlyphAlignment getAlignment () + { + return alignment; + } + + //----------------// + // getComposition // + //----------------// + protected GlyphComposition getComposition () + { + return composition; + } + + //------------// + // getContent // + //------------// + protected GlyphContent getContent () + { + // Lazy allocation, to avoid too many allocations + // (less than 3% of all glyphs need a content facet) + if (content == null) { + addFacet(content = new BasicContent(this)); + } + + return content; + } + + //-----------------// + // internalsString // + //-----------------// + /** + * Return the string of the internals of this class, typically for + * inclusion in a toString. + * The overriding methods, if any, should return a string that begins with + * a " " followed by some content. + * + * @return the string of internals + */ + protected String internalsString () + { + StringBuilder sb = new StringBuilder(25); + + if (getShape() != null) { + sb.append(" ") + .append(recognition.getEvaluation()); + + if (getShape() + .getPhysicalShape() != getShape()) { + sb.append(" physical=") + .append(getShape().getPhysicalShape()); + } + } + + if (getPartOf() != null) { + sb.append(" partOf#") + .append(getPartOf().getId()); + } + + if (getCentroid() != null) { + sb.append(" centroid=[") + .append(getCentroid().x) + .append(",") + .append(getCentroid().y) + .append("]"); + } + + if (isTranslated()) { + sb.append(" trans=[") + .append(getTranslations()) + .append("]"); + } + + if (getResult() != null) { + sb.append(" ") + .append(getResult()); + } + + return sb.toString(); + } + + //----------// + // addFacet // + //----------// + /** + * Register a facet + * + * @param facet the facet to register + */ + final void addFacet (GlyphFacet facet) + { + facets.add(facet); + } +} diff --git a/src/main/omr/glyph/facets/BasicRecognition.java b/src/main/omr/glyph/facets/BasicRecognition.java new file mode 100644 index 0000000..6520258 --- /dev/null +++ b/src/main/omr/glyph/facets/BasicRecognition.java @@ -0,0 +1,310 @@ +//----------------------------------------------------------------------------// +// // +// B a s i c R e c o g n i t i o n // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.glyph.facets; + +import omr.glyph.Evaluation; +import omr.glyph.Shape; +import omr.glyph.ShapeSet; + +import omr.score.entity.TimeRational; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.HashSet; +import java.util.Set; + +/** + * Class {@code BasicRecognition} is the basic implementation of a + * recognition facet. + * + * @author Hervé Bitteur + */ +class BasicRecognition + extends BasicFacet + implements GlyphRecognition +{ + + /** Usual logger utility */ + private static final Logger logger = LoggerFactory.getLogger(BasicRecognition.class); + + /** Current evaluation (shape + grade), if any */ + private Evaluation evaluation; + + /** Set of forbidden shapes, if any */ + private Set forbiddenShapes; + + /** Related time sig rational information, if any */ + private TimeRational timeRational; + + //------------------// + // BasicRecognition // + //------------------// + /** + * Creates a new BasicRecognition object. + * + * @param glyph our glyph + */ + public BasicRecognition (Glyph glyph) + { + super(glyph); + } + + //------------// + // allowShape // + //------------// + @Override + public void allowShape (Shape shape) + { + if (forbiddenShapes != null) { + forbiddenShapes.remove(shape); + } + } + + //--------// + // dumpOf // + //--------// + @Override + public String dumpOf () + { + StringBuilder sb = new StringBuilder(); + + if (evaluation != null) { + sb.append(String.format(" evaluation=%s%n", evaluation)); + } + + Shape physical = (getShape() != null) ? getShape().getPhysicalShape() : null; + if (physical != null) { + sb.append(String.format(" physical=%s%n", physical)); + } + + if (forbiddenShapes != null) { + sb.append(String.format(" forbiddenShapes=%s%n", forbiddenShapes)); + } + + if (timeRational != null) { + sb.append(String.format(" rational=%s%n", timeRational)); + } + + return sb.toString(); + } + + //-------------// + // forbidShape // + //-------------// + @Override + public void forbidShape (Shape shape) + { + if (forbiddenShapes == null) { + forbiddenShapes = new HashSet<>(); + } + + forbiddenShapes.add(shape); + } + + //---------------// + // getEvaluation // + //---------------// + @Override + public Evaluation getEvaluation () + { + return evaluation; + } + + //----------// + // getGrade // + //----------// + @Override + public double getGrade () + { + if (evaluation != null) { + return evaluation.grade; + } else { + // No real interest + return Evaluation.ALGORITHM; + } + } + + //----------// + // getShape // + //----------// + @Override + public Shape getShape () + { + if (evaluation != null) { + return evaluation.shape; + } else { + return null; + } + } + + //-----------------// + // getTimeRational // + //-----------------// + @Override + public TimeRational getTimeRational () + { + return timeRational; + } + + //-------// + // isBar // + //-------// + @Override + public boolean isBar () + { + return ShapeSet.Barlines.contains(getShape()); + } + + //--------// + // isClef // + //--------// + @Override + public boolean isClef () + { + return ShapeSet.Clefs.contains(getShape()); + } + + //---------// + // isKnown // + //---------// + @Override + public boolean isKnown () + { + Shape shape = getShape(); + + return (shape != null) && (shape != Shape.NOISE); + } + + //---------------// + // isManualShape // + //---------------// + @Override + public boolean isManualShape () + { + return getGrade() == Evaluation.MANUAL; + } + + //------------------// + // isShapeForbidden // + //------------------// + @Override + public boolean isShapeForbidden (Shape shape) + { + return (forbiddenShapes != null) && forbiddenShapes.contains(shape); + } + + //--------// + // isStem // + //--------// + @Override + public boolean isStem () + { + return getShape() == Shape.STEM; + } + + //--------// + // isText // + //--------// + @Override + public boolean isText () + { + Shape shape = getShape(); + + return (shape != null) && shape.isText(); + } + + //-------------// + // isWellKnown // + //-------------// + @Override + public boolean isWellKnown () + { + Shape shape = getShape(); + + return (shape != null) && shape.isWellKnown(); + } + + //-----------------// + // resetEvaluation // + //-----------------// + @Override + public void resetEvaluation () + { + evaluation = null; + } + + //---------------// + // setEvaluation // + //---------------// + @Override + public void setEvaluation (Evaluation evaluation) + { + setShape(evaluation.shape, evaluation.grade); + } + + //----------// + // setShape // + //----------// + @Override + public void setShape (Shape shape) + { + setShape(shape, Evaluation.ALGORITHM); + } + + //----------// + // setShape // + //----------// + @Override + public void setShape (Shape shape, + double grade) + { +// // Check status +// if (glyph.isTransient()) { +// logger.error("Setting shape of a transient glyph"); +// } + + // Blacklist the old shape if any + Shape oldShape = getShape(); + + if ((oldShape != null) && (oldShape != shape) + && (oldShape != Shape.GLYPH_PART)) { + forbidShape(oldShape); + + if (glyph.isVip()) { + logger.info("Shape {} forbidden for {}", + oldShape, glyph.idString()); + } + } + + if (shape != null) { + // Remove the new shape from the blacklist if any + allowShape(shape); + } + + // Remember the new shape + evaluation = new Evaluation(shape, grade); + + if (glyph.isVip()) { + logger.info("{} assigned {}", glyph.idString(), evaluation); + } + } + + //-----------------// + // setTimeRational // + //-----------------// + @Override + public void setTimeRational (TimeRational timeRational) + { + this.timeRational = timeRational; + } +} diff --git a/src/main/omr/glyph/facets/BasicTranslation.java b/src/main/omr/glyph/facets/BasicTranslation.java new file mode 100644 index 0000000..c87fad1 --- /dev/null +++ b/src/main/omr/glyph/facets/BasicTranslation.java @@ -0,0 +1,119 @@ +//----------------------------------------------------------------------------// +// // +// B a s i c T r a n s l a t i o n // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.glyph.facets; + +import omr.score.entity.PartNode; + +import java.util.Collection; +import java.util.HashSet; +import java.util.Set; + +/** + * Class {@code BasicTranslation} is the basic implementation of the + * translation facet + * + * @author Hervé Bitteur + */ +class BasicTranslation + extends BasicFacet + implements GlyphTranslation +{ + //~ Instance fields -------------------------------------------------------- + + /** Set of translation(s) of this glyph on the score side */ + private Set translations = new HashSet<>(); + + //~ Constructors ----------------------------------------------------------- + //------------------// + // BasicTranslation // + //------------------// + /** + * Create a new BasicTranslation object + * + * @param glyph our glyph + */ + public BasicTranslation (Glyph glyph) + { + super(glyph); + } + + //~ Methods ---------------------------------------------------------------- + //----------------// + // addTranslation // + //----------------// + @Override + public void addTranslation (PartNode entity) + { + translations.add(entity); + } + + //-------------------// + // clearTranslations // + //-------------------// + @Override + public void clearTranslations () + { + translations.clear(); + } + + //--------// + // dumpOf // + //--------// + @Override + public String dumpOf () + { + StringBuilder sb = new StringBuilder(); + + if (!translations.isEmpty()) { + sb.append(String.format(" translations=%s%n", translations)); + } + + return sb.toString(); + } + + //-----------------// + // getTranslations // + //-----------------// + @Override + public Collection getTranslations () + { + return translations; + } + + //-----------------// + // invalidateCache // + //-----------------// + @Override + public void invalidateCache () + { + clearTranslations(); + } + + //--------------// + // isTranslated // + //--------------// + @Override + public boolean isTranslated () + { + return !translations.isEmpty(); + } + + //----------------// + // setTranslation // + //----------------// + @Override + public void setTranslation (PartNode entity) + { + translations.clear(); + addTranslation(entity); + } +} diff --git a/src/main/omr/glyph/facets/Glyph.java b/src/main/omr/glyph/facets/Glyph.java new file mode 100644 index 0000000..f4a13e1 --- /dev/null +++ b/src/main/omr/glyph/facets/Glyph.java @@ -0,0 +1,164 @@ +//----------------------------------------------------------------------------// +// // +// G l y p h // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.glyph.facets; + +import omr.check.Checkable; + +import java.awt.Point; +import java.util.Comparator; + +/** + * Interface {@code Glyph} represents any glyph found, such as stem, + * ledger, accidental, note head, word, text line, etc... + * + *

A Glyph is basically a collection of sections. It can be split into + * smaller glyphs, which may later be re-assembled into another instance of + * glyph. There is a means, based on a simple signature (weight and moments) + * to detect if the glyph at hand is identical to a previous one, which is + * then reused. + * + *

A Glyph can be stored on disk and reloaded in order to train a glyph + * evaluator. + * + * @author Hervé Bitteur + */ +public interface Glyph + extends + /** For handling check results */ + Checkable, + /** For id and related lag */ + GlyphAdministration, + /** For member sections */ + GlyphComposition, + /** For display color */ + GlyphDisplay, + /** For items nearby */ + GlyphEnvironment, + /** For physical appearance */ + GlyphGeometry, + /** For shape assignment */ + GlyphRecognition, + /** For translation to score items */ + GlyphTranslation, + /** For mean line */ + GlyphAlignment, + /** For textual content */ + GlyphContent +{ + //~ Static fields/initializers --------------------------------------------- + + /** For comparing glyphs according to their height. */ + public static final Comparator byHeight = new Comparator() + { + @Override + public int compare (Glyph o1, + Glyph o2) + { + return o1.getBounds().height - o2.getBounds().height; + } + }; + + /** For comparing glyphs according to their decreasing weight. */ + public static final Comparator byReverseWeight = new Comparator() + { + @Override + public int compare (Glyph o1, + Glyph o2) + { + return o2.getWeight() - o1.getWeight(); + } + }; + + /** For comparing glyphs according to their id. */ + public static final Comparator byId = new Comparator() + { + @Override + public int compare (Glyph o1, + Glyph o2) + { + return o1.getId() - o2.getId(); + } + }; + + /** For comparing glyphs according to their abscissa, + * then ordinate, then id. */ + public static final Comparator byAbscissa = new Comparator() + { + @Override + public int compare (Glyph o1, + Glyph o2) + { + if (o1 == o2) { + return 0; + } + + Point ref = o1.getBounds() + .getLocation(); + Point otherRef = o2.getBounds() + .getLocation(); + + // Are x values different? + int dx = ref.x - otherRef.x; + + if (dx != 0) { + return dx; + } + + // Vertically aligned, so use ordinates + int dy = ref.y - otherRef.y; + + if (dy != 0) { + return dy; + } + + // Finally, use id ... + return o1.getId() - o2.getId(); + } + }; + + /** For comparing glyphs according to their ordinate, + * then abscissa, then id. */ + public static final Comparator ordinateComparator = new Comparator() + { + @Override + public int compare (Glyph o1, + Glyph o2) + { + if (o1 == o2) { + return 0; + } + + Point ref = o1.getBounds() + .getLocation(); + Point otherRef = o2.getBounds() + .getLocation(); + + // Are y values different? + int dy = ref.y - otherRef.y; + + if (dy != 0) { + return dy; + } + + // Horizontally aligned, so use abscissae + int dx = ref.x - otherRef.x; + + if (dx != 0) { + return dx; + } + + // Finally, use id ... + return o1.getId() - o2.getId(); + } + }; + +} diff --git a/src/main/omr/glyph/facets/GlyphAdministration.java b/src/main/omr/glyph/facets/GlyphAdministration.java new file mode 100644 index 0000000..d2e9c03 --- /dev/null +++ b/src/main/omr/glyph/facets/GlyphAdministration.java @@ -0,0 +1,95 @@ +//----------------------------------------------------------------------------// +// // +// G l y p h A d m i n i s t r a t i o n // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.glyph.facets; + +import omr.glyph.Nest; + +import omr.util.Vip; + +/** + * Interface {@code GlyphAdministration} defines the administration + * facet of a glyph, handling the glyph id and its related containing + * nest. + * + * @author Hervé Bitteur + */ +interface GlyphAdministration + extends GlyphFacet, Vip +{ + //~ Methods ---------------------------------------------------------------- + + /** + * Report whether this entity has been "processed". + * + * @return the processed flag value + */ + public boolean isProcessed (); + + /** + * Set a flag to be used at caller's will. + * + * @param processed the processed to set + */ + public void setProcessed (boolean processed); + + /** + * Report the unique glyph id within its containing nest + * + * @return the glyph id + */ + int getId (); + + /** + * Report the containing nest + * + * @return the containing nest + */ + Nest getNest (); + + /** + * Report a short glyph reference + * + * @return glyph reference + */ + String idString (); + + /** + * Test whether the glyph is transient (not yet inserted into the nest) + * + * @return true if transient + */ + boolean isTransient (); + + /** + * Report whether this glyph is virtual (rather than real) + * + * @return true if virtual + */ + boolean isVirtual (); + + /** + * Assign a unique ID to the glyph + * + * @param id the unique id + */ + void setId (int id); + + /** + * The setter for glyph nest. + * To be used with care, it freezes the glyph geometry, forbidding any + * geometric modification (such as {@link Glyph#addSection} that would + * impact its signature. + * + * @param nest the containing nest + */ + void setNest (Nest nest); +} diff --git a/src/main/omr/glyph/facets/GlyphAlignment.java b/src/main/omr/glyph/facets/GlyphAlignment.java new file mode 100644 index 0000000..431b9ad --- /dev/null +++ b/src/main/omr/glyph/facets/GlyphAlignment.java @@ -0,0 +1,199 @@ +//----------------------------------------------------------------------------// +// // +// G l y p h A l i g n m e n t // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.glyph.facets; + +import omr.math.Line; + +import omr.run.Orientation; + +import java.awt.Graphics2D; +import java.awt.Rectangle; +import java.awt.geom.Point2D; + +/** + * Interface {@code GlyphAlignment} describes glyph alignment. + * The key feature is the approximating Line on all points of the glyph. + * The line can be the least-square fitted line, or a natural spline for more + * complex cases. + * + *

    + *
  • Staff lines, ledgers, alternate ends are examples of rather + * horizontal glyphs.
  • + *
  • Bar lines, stems are examples of rather vertical glyphs.
  • + *
  • Other glyphs have no dominant orientation.
  • + *
+ * + *

Note that a glyph has no predefined orientation, only the slope of its + * approximating line is relevant and allows to disambiguate between the + * start point and the stop point. If abs(tangent) is less than 45 degrees we + * have a rather horizontal glyph, otherwise a rather vertical glyph.

+ * + * @author Hervé Bitteur + */ +public interface GlyphAlignment + extends GlyphFacet +{ + //~ Methods ---------------------------------------------------------------- + + /** + * Report the average thickness, using the provided orientation. + * + * @return the average thickness + */ + public double getMeanThickness (Orientation orientation); + + /** + * Report the ratio of length over thickness, using provided + * orientation. + * + * @param orientation the general orientation reference + * @return the "slimness" of the glyph + * @see #getLength + * @see #getThickness + */ + double getAspect (Orientation orientation); + + /** + * Compute the number of pixels stuck on first side of the glyph. + * + * @return the number of pixels + */ + int getFirstStuck (); + + /** + * Report the co-tangent of glyph line angle with abscissa axis + * + * @return co-tangent of heading angle (dx/dy). + */ + double getInvertedSlope (); + + /** + * Compute the nb of pixels stuck on last side of the glyph. + * + * @return the number of pixels + */ + int getLastStuck (); + + /** + * Report the length of the glyph, along the provided orientation. + * + * @param orientation the general orientation reference + * @return the glyph length in pixels + */ + int getLength (Orientation orientation); + + /** + * Return the approximating line computed on the glyph, as an + * absolute line, with x for horizontal axis and y for + * vertical axis. + * + * @return The absolute line + */ + Line getLine (); + + /** + * Return the mean quadratic distance of the defining population + * of points to the resulting line. + * This can be used to measure how well the line fits the points. + * + * @return the absolute value of the mean distance + */ + double getMeanDistance (); + + /** + * Return the position at the middle of the glyph, + * using the provided orientation. + * + * @return the position of the middle of the glyph + */ + int getMidPos (Orientation orientation); + + /** + * Report the precise glyph position for the provided coordinate. + * + * @param coord the coord value (x for horizontal, y for vertical) + * @param orientation the general orientation reference + * @return the pos value (y for horizontal, x for vertical) + */ + double getPositionAt (double coord, + Orientation orientation); + + /** + * Report the absolute centroid of all the glyph pixels found in the + * provided absolute ROI + * + * @param absRoi the desired absolute region of interest + * @return the absolute barycenter of the pixels found + */ + Point2D getRectangleCentroid (Rectangle absRoi); + + /** + * Report the tangent of glyph line angle with abscissa axis + * + * @return tangent of heading angle (dy/dx). + */ + double getSlope (); + + /** + * Report the absolute point at the beginning (with respect to the provided + * orientation) of the approximating line. + * + * @param orientation the general orientation reference + * @return the starting point of the glyph line + */ + Point2D getStartPoint (Orientation orientation); + + /** + * Report the absolute point at the end (with respect to the provided + * orientation) of the approximating line. + * + * @param orientation the general orientation reference + * @return the ending point of the line + */ + Point2D getStopPoint (Orientation orientation); + + /** + * Report the glyph thickness across the desired orientation. + * + * @param orientation the general orientation reference + * @return the thickness in pixels + */ + int getThickness (Orientation orientation); + + /** + * Report the resulting thickness of this glyph at the provided + * coordinate, using a predefined probe width. + * + * @param coord the desired abscissa + * @param orientation the general orientation reference + * @return the thickness measured, expressed in number of pixels. + */ + double getThicknessAt (double coord, + Orientation orientation); + + /** + * Render the main guiding line of the glyph, using the current + * foreground color. + * + * @param g the graphic context + */ + void renderLine (Graphics2D g); + + /** + * Force the locations of start point and stop points. + * + * @param pStart new start point + * @param pStop new stop point + */ + void setEndingPoints (Point2D pStart, + Point2D pStop); +} diff --git a/src/main/omr/glyph/facets/GlyphComposition.java b/src/main/omr/glyph/facets/GlyphComposition.java new file mode 100644 index 0000000..7ae3df8 --- /dev/null +++ b/src/main/omr/glyph/facets/GlyphComposition.java @@ -0,0 +1,173 @@ +//----------------------------------------------------------------------------// +// // +// G l y p h C o m p o s i t i o n // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.glyph.facets; + +import omr.check.Result; + +import omr.lag.Section; + +import omr.sheet.SystemInfo; + +import java.util.SortedSet; + +/** + * Interface {@code GlyphComposition} defines the facet that handles + * the way a glyph is composed of sections members, as well as the + * relationships with sub-glyphs (parts) if any. + * + * @author Hervé Bitteur + */ +public interface GlyphComposition + extends GlyphFacet +{ + //~ Enumerations ----------------------------------------------------------- + + /** Specifies whether a section must point back to a containing glyph */ + enum Linking + { + //~ Enumeration constant initializers ---------------------------------- + + /** Make the section point back to the containing glyph */ + LINK_BACK, + /** Do + * not make the section point back to the containing glyph */ + NO_LINK_BACK; + + } + + //~ Methods ---------------------------------------------------------------- + /** + * Report the top ancestor of this glyph. + * This is this glyph itself, when it has no parent (i.e. not been included + * into another one) + * + * @return the glyph ancestor + */ + public Glyph getAncestor (); + + /** + * Report the containing compound, if any, which has "stolen" the + * sections of this glyph. + * + * @return the containing compound if any + */ + public Glyph getPartOf (); + + /** + * Record the link to the compound which has "stolen" the sections + * of this glyph. + * + * @param compound the containing compound, if any + */ + public void setPartOf (Glyph compound); + + /** + * Add a section as a member of this glyph. + * + * @param section The section to be included + * @param link While adding a section to this glyph members, should we + * also + * set the link from section back to the glyph? + */ + void addSection (Section section, + Linking link); + + /** + * Debug function that returns true if this glyph contains the + * section whose ID is provided. + * + * @param id the ID of interesting section + * @return true if such section exists among glyph sections + */ + boolean containsSection (int id); + + /** + * Cut the link to this glyph from its member sections, only if the + * sections actually point to this glyph. + */ + void cutSections (); + + /** + * Check whether all the glyph sections belong to the same system. + * + * @param system the supposed containing system + * @return the alien system found, or null if OK + */ + SystemInfo getAlienSystem (SystemInfo system); + + /** + * Report the first section in the ordered collection of members. + * + * @return the first section of the glyph + */ + Section getFirstSection (); + + /** + * Report the set of member sections. + * + * @return member sections + */ + SortedSet
getMembers (); + + /** + * Report the result found during analysis of this glyph. + * + * @return the analysis result + */ + Result getResult (); + + /** + * Tests whether this glyph is active. + * (all its member sections point to it) + * + * @return true if glyph is active, false otherwise + */ + boolean isActive (); + + /** + * Check whether the glyph is successfully recognized. + * + * @return true if the glyph is successfully recognized + */ + boolean isSuccessful (); + + /** + * Make all the glyph's sections point back to this glyph. + */ + void linkAllSections (); + + /** + * Remove a section from the glyph members + * + * @param section the section to remove + * @param link should we update the link from section to glyph? + * @return true if the section was actually found and removed + */ + boolean removeSection (Section section, + Linking link); + + /** + * Record the analysis result in the glyph itself. + * + * @param result the assigned result + */ + void setResult (Result result); + + /** + * Include the sections from another glyph into this one, and make + * its sections point into this one. + * Doing so, the other glyph becomes inactive. + * + * @param that the glyph to swallow + */ + void stealSections (Glyph that); +} diff --git a/src/main/omr/glyph/facets/GlyphContent.java b/src/main/omr/glyph/facets/GlyphContent.java new file mode 100644 index 0000000..d314e5b --- /dev/null +++ b/src/main/omr/glyph/facets/GlyphContent.java @@ -0,0 +1,113 @@ +//----------------------------------------------------------------------------// +// // +// G l y p h C o n t e n t // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.glyph.facets; + +import omr.text.TextRoleInfo; +import omr.text.TextWord; + +import java.awt.Point; + +/** + * Interface {@code GlyphContent} defines a facet that deals with the + * textual content, if any, of a glyph. + * + * @author Hervé Bitteur + */ +public interface GlyphContent + extends GlyphFacet +{ + //~ Instance fields -------------------------------------------------------- + + /** String equivalent of Character used for elision. (undertie) */ + String ELISION_STRING = new String(Character.toChars(8255)); + + /** String equivalent of Character used for extension. (underscore) */ + String EXTENSION_STRING = "_"; + + /** String equivalent of Character used for hyphen. */ + String HYPHEN_STRING = "-"; + + //~ Methods ---------------------------------------------------------------- + /** + * Report the manually assigned role, if any. + * + * @return the manual role for this glyph, or null + */ + TextRoleInfo getManualRole (); + + /** + * Report the manually assigned text, if any. + * + * @return the manual string value for this glyph, or null + */ + String getManualValue (); + + /** + * Report the current language, if any, defined for this glyph. + * + * @return the current glyph language code, or null + */ + String getOcrLanguage (); + + /** + * Report the starting point of this text glyph, which is the left + * side abscissa and the baseline ordinate. + * + * @return the starting point of the text glyph, specified in pixels + */ + Point getTextLocation (); + + /** + * Report the text role of the textual glyph within the score. + * + * @return the role of this textual glyph + */ + TextRoleInfo getTextRole (); + + /** + * Report the string value of this text glyph if any. + * + * @return the text meaning of this glyph if any, which is the manual value + * if any, or the ocr value otherwise. + */ + String getTextValue (); + + /** + * Return the corresponding text word for this glyph, if any. + * + * @return the related text word, null otherwise. + */ + TextWord getTextWord (); + + /** + * Manually assign a text role to the glyph. + * + * @param manualRole the role for this text glyph + */ + void setManualRole (TextRoleInfo manualRole); + + /** + * Manually assign a text meaning to the glyph. + * + * @param manualValue the string value for this text glyph + */ + void setManualValue (String manualValue); + + /** + * Set the related text word. + * + * @param ocrLanguage the language provided to OCR engine for recognition + * @param textWord the TextWord for this glyph + */ + void setTextWord (String ocrLanguage, + TextWord textWord); +} diff --git a/src/main/omr/glyph/facets/GlyphDisplay.java b/src/main/omr/glyph/facets/GlyphDisplay.java new file mode 100644 index 0000000..d6ad625 --- /dev/null +++ b/src/main/omr/glyph/facets/GlyphDisplay.java @@ -0,0 +1,76 @@ +//----------------------------------------------------------------------------// +// // +// G l y p h D i s p l a y // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.glyph.facets; + +import omr.glyph.ui.AttachmentHolder; + +import omr.lag.Section; + +import java.awt.Color; +import java.awt.image.BufferedImage; +import java.util.Collection; + +/** + * Interface {@code GlyphDisplay} defines the facet which handles the + * way a glyph is displayed (its color, its image). + * + * @author Hervé Bitteur + */ +interface GlyphDisplay + extends GlyphFacet, AttachmentHolder +{ + //~ Methods ---------------------------------------------------------------- + + /** + * Report a basic representation of the glyph, using ascii chars. + * + * @return an ascii representation + */ + String asciiDrawing (); + + /** + * Set the display color of all sections that compose this glyph. + * + * @param color color for the whole glyph + */ + void colorize (Color color); + + /** + * Set the display color of all sections in provided collection. + * + * @param sections the collection of sections + * @param color the display color + */ + void colorize (Collection
sections, + Color color); + + /** + * Report the color to be used to colorize the provided glyph, + * according to the color policy which is based on the glyph shape. + * + * @return the related shape color of the glyph, or the predefined {@link + * omr.ui.Colors#SHAPE_UNKNOWN} if the glyph has no related shape + */ + Color getColor (); + + /** + * Report an image of the glyph (which can be handed to the OCR) + * + * @return a black & white image (contour box size ) + */ + BufferedImage getImage (); + + /** + * Reset the display color of all sections that compose this glyph. + */ + void recolorize (); +} diff --git a/src/main/omr/glyph/facets/GlyphEnvironment.java b/src/main/omr/glyph/facets/GlyphEnvironment.java new file mode 100644 index 0000000..a27f3ee --- /dev/null +++ b/src/main/omr/glyph/facets/GlyphEnvironment.java @@ -0,0 +1,158 @@ +//----------------------------------------------------------------------------// +// // +// G l y p h E n v i r o n m e n t // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.glyph.facets; + +import omr.lag.Lag; +import omr.lag.Section; + +import omr.sheet.SystemInfo; + +import omr.util.HorizontalSide; +import omr.util.Predicate; + +import java.awt.Rectangle; +import java.util.Set; + +/** + * Interface {@code GlyphEnvironment} defines the facet in charge of + * the surrounding environment of a glyph, in terms of staff-based + * pitch position, of presence of stem or ledgers, etc. + * + * @author Hervé Bitteur + */ +interface GlyphEnvironment + extends GlyphFacet +{ + //~ Methods ---------------------------------------------------------------- + + /** + * Forward stem-related information from the provided glyph + * + * @param glyph the glyph whose stem information has to be used + */ + void copyStemInformation (Glyph glyph); + + /** + * Report the number of alien pixels, from the provided lag, found + * in the specified absolute roi + * + * @param lag the lag to serach + * @param absRoi the absolute region of interest + * @param predicate optional predicate to further filter these aliens + * @return the number of alien pixels found + */ + int getAlienPixelsFrom (Lag lag, + Rectangle absRoi, + Predicate
predicate); + + /** + * Report the set of glyphs that are connected to this one + * + * @return the set of neighboring glyphs, connected through their sections + */ + Set getConnectedNeighbors (); + + /** + * Report the first stem attached (left then right), if any + * + * @return first stem found, or null + */ + Glyph getFirstStem (); + + /** + * Report the pitchPosition feature (position relative to the staff) + * + * @return the pitchPosition value + */ + double getPitchPosition (); + + /** + * Report the stem attached on the provided side, if any + * + * @return stem on provided side, or null + */ + Glyph getStem (HorizontalSide side); + + /** + * Report the number of stems the glyph is close to + * + * @return the number of stems near by, typically 0, 1 or 2. + */ + int getStemNumber (); + + /** + * Return the known glyphs stuck on last side of the stick. + * (this is relevant mainly for a stem glyph) + * + * @param predicate the predicate to apply on each glyph + * @param goods the set of correct glyphs (perhaps empty) + * @param bads the set of non-correct glyphs (perhaps empty) + */ + void getSymbolsAfter (Predicate predicate, + Set goods, + Set bads); + + /** + * Return the known glyphs stuck on first side of the stick. + * (this is relevant mainly for a stem glyph) + * + * @param predicate the predicate to apply on each glyph + * @param goods the set of correct glyphs (perhaps empty) + * @param bads the set of non-correct glyphs (perhaps empty) + */ + void getSymbolsBefore (Predicate predicate, + Set goods, + Set bads); + + /** + * Report the containing system, if any. + * + * @return the system containing this glyph + */ + SystemInfo getSystem (); + + /** + * Report whether the glyph touches a ledger + * + * @return true if there is a close ledger + */ + boolean isWithLedger (); + + /** + * Setter for the pitch position, with respect to containing staff + * + * @param pitchPosition the pitch position wrt the staff + */ + void setPitchPosition (double pitchPosition); + + /** + * Assign the stem on the provided side + * + * @param stem stem glyph + */ + void setStem (Glyph stem, + HorizontalSide side); + + /** + * Remember the number of stems near by + * + * @param stemNumber the number of stems + */ + void setStemNumber (int stemNumber); + + /** + * Remember info about ledger nearby + * + * @param withLedger true is there is such ledger + */ + void setWithLedger (boolean withLedger); +} diff --git a/src/main/omr/glyph/facets/GlyphFacet.java b/src/main/omr/glyph/facets/GlyphFacet.java new file mode 100644 index 0000000..6b977e9 --- /dev/null +++ b/src/main/omr/glyph/facets/GlyphFacet.java @@ -0,0 +1,34 @@ +//----------------------------------------------------------------------------// +// // +// G l y p h F a c e t // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +package omr.glyph.facets; + +/** + * Interface {@code GlyphFacet} is the root of any Glyph facet, + * gathering utility methods to be provided by each facet. + * + * @author Hervé Bitteur + */ +interface GlyphFacet +{ + //~ Methods ---------------------------------------------------------------- + + /** + * Dump of facet internal data. + * + * @return Internal representation + */ + String dumpOf (); + + /** + * Reset relevant cached data. + */ + void invalidateCache (); +} diff --git a/src/main/omr/glyph/facets/GlyphGeometry.java b/src/main/omr/glyph/facets/GlyphGeometry.java new file mode 100644 index 0000000..4539c8e --- /dev/null +++ b/src/main/omr/glyph/facets/GlyphGeometry.java @@ -0,0 +1,202 @@ +//----------------------------------------------------------------------------// +// // +// G l y p h G e o m e t r y // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.glyph.facets; + +import omr.glyph.GlyphSignature; + +import omr.math.Circle; +import omr.math.PointsCollector; + +import omr.moments.ARTMoments; +import omr.moments.GeometricMoments; + +import java.awt.Point; +import java.awt.Rectangle; + +/** + * Interface {@code GlyphGeometry} defines the facet which handles all + * the geometrical characteristics of a glyph (scale, contour box, + * location, weight, density, moments, etc). + * + *

Nota: A glyph, unlike its member sections, has no orientation, so all the + * following methods work in absolute coordinates. + * + * @author Hervé Bitteur + */ +interface GlyphGeometry + extends GlyphFacet +{ + //~ Methods ---------------------------------------------------------------- + + /** + * Report the glyph ART moments, which are lazily computed. + * + * @return the glyph ART moments + */ + ARTMoments getARTMoments (); + + /** + * Report the glyph area center. + * (The point is lazily evaluated). + * + * @return the area center point + */ + Point getAreaCenter (); + + /** + * Return a copy of the absolute display bounding box. + * Useful to quickly check if the glyph needs to be repainted. + * + * @return a COPY of the bounding contour rectangle box + */ + Rectangle getBounds (); + + /** + * Report the glyph absolute centroid (mass center). + * The point is lazily evaluated. + * + * @return the absolute mass center point + */ + Point getCentroid (); + + /** + * Report the approximating circle, if any. + * + * @return the approximating circle, or null + */ + Circle getCircle (); + + /** + * Report the density of the stick, that is its weight divided by + * the area of its bounding rectangle. + * + * @return the density + */ + double getDensity (); + + /** + * Report the glyph geometric moments, which are lazily computed. + * + * @return the glyph geometric moments + */ + GeometricMoments getGeometricMoments (); + + /** + * Report the interline value for the glyph containing staff, + * which is used for some of the moments. + * + * @return the interline value + */ + int getInterline (); + + /** + * Report the glyph (reference) location, which is the equivalent + * of the icon reference point if one such point exists, or the + * glyph area center otherwise. + * The point is lazily evaluated. + * + * @return the reference center point + */ + Point getLocation (); + + /** + * Report the height of this glyph, after normalization to sheet + * interline. + * + * @return the height value, expressed as an interline fraction + */ + double getNormalizedHeight (); + + /** + * Report the weight of this glyph, after normalization to sheet + * interline. + * + * @return the weight value, expressed as an interline square fraction + */ + double getNormalizedWeight (); + + /** + * Report the width of this glyph, after normalization to sheet + * interline. + * + * @return the width value, expressed as an interline fraction + */ + double getNormalizedWidth (); + + /** + * Report the collector filled with glyph points. + * + * @return the populated points collector + */ + PointsCollector getPointsCollector (); + + /** + * Report the last registration signature. + * + * @return the previous valid glyph signature + */ + GlyphSignature getRegisteredSignature (); + + /** + * Report current signature that distinguishes this glyph. + * + * @return the glyph signature + */ + GlyphSignature getSignature (); + + /** + * Report the total weight of this glyph, as the sum of its section + * weights. + * + * @return the total weight (number of pixels) + */ + int getWeight (); + + /** + * Check whether the glyph intersect the provided absolute + * rectangle. + * + * @param rectangle the provided absolute rectangle + * @return true if intersection is not empty, false otherwise + */ + boolean intersects (Rectangle rectangle); + + /** + * Remember an approximating circle. + * + * @param circle the circle value, or null + */ + void setCircle (Circle circle); + + /** + * Force the glyph contour box (when start and stop points are + * forced). + * + * @param contourBox the forced contour box + */ + void setContourBox (Rectangle contourBox); + + /** + * Remember registration signature. + * + * @param sig the signature used for registration + */ + void setRegisteredSignature (GlyphSignature sig); + + /** + * Apply a translation to the glyph from its current location, + * according to the provided vector. + * + * @param vector the (dx, dy) translation + */ + void translate (Point vector); +} diff --git a/src/main/omr/glyph/facets/GlyphRecognition.java b/src/main/omr/glyph/facets/GlyphRecognition.java new file mode 100644 index 0000000..a3dd6ad --- /dev/null +++ b/src/main/omr/glyph/facets/GlyphRecognition.java @@ -0,0 +1,171 @@ +//----------------------------------------------------------------------------// +// // +// G l y p h R e c o g n i t i o n // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.glyph.facets; + +import omr.glyph.Evaluation; +import omr.glyph.Shape; + +import omr.score.entity.TimeRational; + +/** + * Interface {@code GlyphRecognition} defines a facet that deals with + * the shape recognition of a glyph. + * + * @author Hervé Bitteur + */ +interface GlyphRecognition + extends GlyphFacet +{ + //~ Methods ---------------------------------------------------------------- + + /** + * Remove the provided shape from the collection of forbidden + * shaped, if any. + * + * @param shape the shape to allow + */ + void allowShape (Shape shape); + + /** + * Forbid a specific shape. + * + * @param shape the shape to forbid + */ + void forbidShape (Shape shape); + + /** + * Report the evaluation, if any. + * + * @return the evaluation structure (shape + grade + failure if any) + */ + Evaluation getEvaluation (); + + /** + * Report the grade of the glyph shape. + * + * @return the grade related to glyph shape + */ + double getGrade (); + + /** + * Report the registered glyph shape. + * + * @return the glyph shape, which may be null + */ + Shape getShape (); + + /** + * Report the related timesig rational if any. + * + * @return the time rational + */ + TimeRational getTimeRational (); + + /** + * Convenient method which tests if the glyph is a Bar line. + * + * @return true if glyph shape is a bar + */ + boolean isBar (); + + /** + * Convenient method which tests if the glyph is a Clef. + * + * @return true if glyph shape is a Clef + */ + boolean isClef (); + + /** + * A glyph is considered as known if it has a registered shape other + * than NOISE. + * (Notice that CLUTTER as well as NO_LEGAL_TIME and GLYPH_PART are + * considered as being known). + * + * @return true if known + */ + boolean isKnown (); + + /** + * Report whether the shape of this glyph has been manually assigned. + * (and thus can only be modified by explicit user action). + * + * @return true if shape manually assigned + */ + boolean isManualShape (); + + /** + * Check whether a shape is forbidden for this glyph. + * + * @param shape the shape to check + * @return true if the provided shape is one of the forbidden shapes for + * this glyph + */ + boolean isShapeForbidden (Shape shape); + + /** + * Check whether the glyph shape is a Stem. + * + * @return true if glyph shape is a Stem + */ + boolean isStem (); + + /** + * Check whether the glyph shape is a text. + * + * @return true if text or character + */ + boolean isText (); + + /** + * A glyph is considered as well known if it has a registered well + * known shape. + * + * @return true if so + */ + boolean isWellKnown (); + + /** + * Nullify the current evaluation, without impact on forbidden + * shapes, to allow a new evaluation computation. + */ + void resetEvaluation (); + + /** + * Assign an evaluation. + * + * @param evaluation the evaluation structure, perhaps null + */ + void setEvaluation (Evaluation evaluation); + + /** + * Setter for the glyph shape (Algorithm assumed). + * + * @param shape the assigned shape, which may be null + */ + void setShape (Shape shape); + + /** + * Setter for the glyph shape, with related grade. + * + * @param shape the assigned shape + * @param grade the related grade + */ + void setShape (Shape shape, + double grade); + + /** + * Set the glyph timesig rational value. + * + * @param timeRational the time rational to set + */ + void setTimeRational (TimeRational timeRational); +} diff --git a/src/main/omr/glyph/facets/GlyphTranslation.java b/src/main/omr/glyph/facets/GlyphTranslation.java new file mode 100644 index 0000000..7094361 --- /dev/null +++ b/src/main/omr/glyph/facets/GlyphTranslation.java @@ -0,0 +1,61 @@ +//----------------------------------------------------------------------------// +// // +// G l y p h T r a n s l a t i o n // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.glyph.facets; + +import omr.score.entity.PartNode; + +import java.util.Collection; + +/** + * Interface {@code GlyphTranslation} defines a facet dealing with the + * translation of a glyph into its score entity counter-part(s). + * + * @author Hervé Bitteur + */ +interface GlyphTranslation + extends GlyphFacet +{ + //~ Methods ---------------------------------------------------------------- + + /** + * Add a score entity as a translation for this glyph + * + * @param entity the counterpart of this glyph on the score side + */ + void addTranslation (PartNode entity); + + /** + * Remove all the links to score entities + */ + void clearTranslations (); + + /** + * Report the collection of score entities this glyph contributes to + * + * @return the collection of entities that are translations of this glyph + */ + Collection getTranslations (); + + /** + * Report whether this glyph is translated to a score entity + * + * @return true if this glyph is translated to score + */ + boolean isTranslated (); + + /** + * Assign a unique score translation for this glyph + * + * @param entity the score entity that is a translation of this glyph + */ + void setTranslation (PartNode entity); +} diff --git a/src/main/omr/glyph/facets/GlyphValue.java b/src/main/omr/glyph/facets/GlyphValue.java new file mode 100644 index 0000000..7ed6a44 --- /dev/null +++ b/src/main/omr/glyph/facets/GlyphValue.java @@ -0,0 +1,130 @@ +//----------------------------------------------------------------------------// +// // +// G l y p h V a l u e // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.glyph.facets; + +import omr.glyph.Shape; + +import omr.lag.Section; + +import java.util.SortedSet; + +import javax.xml.bind.annotation.XmlAttribute; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlRootElement; + +/** + * Class {@code GlyphValue} is used to map a Glyph with its XML + * representation as handled by JAXB, to allow the decoupling between + * in-memory layout and XML layout. + * + * @author Hervé Bitteur + */ +@XmlRootElement(name = "glyph") +public class GlyphValue +{ + //~ Instance fields -------------------------------------------------------- + + /** Id */ + @XmlAttribute(name = "id") + final int id; + + /** Interline */ + @XmlAttribute(name = "interline") + final int interline; + + /** Shape */ + @XmlAttribute(name = "shape") + final Shape shape; + + /** Stem Number */ + @XmlElement(name = "stem-number") + final int stemNumber; + + /** With Ledger */ + @XmlElement(name = "with-ledger") + final boolean withLedger; + + /** Pitch Position */ + @XmlElement(name = "pitch-position") + final double pitchPosition; + + /** Member sections */ + @XmlElement(name = "section") + final SortedSet

members; + + //~ Constructors ----------------------------------------------------------- + //------------// + // GlyphValue // + //------------// + /** + * Create a new GlyphValue object from scratch + * + * @param shape + * @param interline + * @param id + * @param stemNumber + * @param withLedger + * @param pitchPosition + * @param members + */ + public GlyphValue (Shape shape, + int interline, + int id, + int stemNumber, + boolean withLedger, + double pitchPosition, + SortedSet
members) + { + this.shape = shape; + this.interline = interline; + this.id = id; + this.stemNumber = stemNumber; + this.withLedger = withLedger; + this.pitchPosition = pitchPosition; + this.members = members; + } + + /** + * Create a new GlyphValue object from a real Glyph + * + * @param glyph the real glyph + */ + public GlyphValue (Glyph glyph) + { + this( + (glyph.getShape() != null) ? glyph.getShape().getPhysicalShape() + : null, + glyph.getInterline(), + glyph.getId(), + glyph.getStemNumber(), + glyph.isWithLedger(), + glyph.getPitchPosition(), + glyph.getMembers()); + } + + //------------// + // GlyphValue // + //------------// + /** + * No-arg constructor needed for JAXB, using dummy values + */ + protected GlyphValue () + { + this.shape = null; + this.interline = 0; + this.id = 0; + this.stemNumber = 0; + this.withLedger = false; + this.pitchPosition = 0; + this.members = null; + } +} diff --git a/src/main/omr/glyph/facets/package.html b/src/main/omr/glyph/facets/package.html new file mode 100644 index 0000000..9b13c1c --- /dev/null +++ b/src/main/omr/glyph/facets/package.html @@ -0,0 +1,17 @@ + + + + + + Package facets + + + + +

+ Package dedicated to the subdivision of the large Glyph interface into + specialized smaller facets +

+ + diff --git a/src/main/omr/glyph/package.html b/src/main/omr/glyph/package.html new file mode 100644 index 0000000..82dbc9a --- /dev/null +++ b/src/main/omr/glyph/package.html @@ -0,0 +1,108 @@ + + + + + + Package omr.glyph + + + +

+ The glyph package deals with assemblies of sections of foreground + pixels that are matched against predefined shapes, knowing that + these glyphs can be self-standing symbols (such as a clef) or + leaves in larger constructions (such as hote heads and stem in a + chord). +

+ +

+ Glyph Data Model +

+

+ Glyph Data Model +

+

+ Glyph Life Cycle +

+
+ Creation +
+
    +
  • Many glyphs are created through the {@link + omr.glyph.GlyphsBuilder#retrieveGlyphs} method which builds glyphs + out of connected unknown sections, and then use {@link + omr.glyph.GlyphsBuilder#addGlyph} to actually record the glyph into + the lag and its containing system. +
  • +
  • Some glyphs are created by building a compound out of other + glyphs, by {@link omr.glyph.GlyphsBuilder#buildTransientCompound}. + These compounds have no effect until they are inserted via {@link + omr.glyph.GlyphsBuilder#addGlyph}. +
  • +
+
+ Insertion +
+ +

+ This is done by {@link omr.glyph.GlyphsBuilder#addGlyph} which + performs the following actions: +

+
    +
  1. If the glyph is a compound (made of other glyphs) then its + parts are made pointing back to the compound, they are removed from + the system collection of glyphs, and their member sections are made + pointing to the compound. Doing so, the former parts are no longer + "active", only the compound is "active". +
  2. +
  3. The glyph is recorded in the lag, and if found identical to + some existing glyph, the existing glyph is returned, otherwise this + new glyph is recorded and assigned a lag-based unique ID. +
  4. +
  5. Finally, the glyph is recorded in the system collection of + glyphs. +
  6. +
+ +
+ Deletion +
+

+ A (recorded) glyph is never totally deleted, it can be reused then + by chance an identical glyph is inserted into the lag. This allows + to cache the glyph parameters, moments, forbidden shapes, OCR + content, etc. +

+

+ What {@link omr.glyph.GlyphsBuilder#removeGlyph} does is simply: +

+
    + +
  1. Removing the glyph from the system collection of glyphs. +
  2. +
  3. Cutting the link from its member sections, making this glyph + "inactive". +
  4. +
+

+ Glyph display +

+

+ A glyph is made visible via its member sections (except for the + "selected" glyph(s) which use a specific highlighting mode). + Several glyphs may contain the same section, but since the section + can point back to at most one glyph, there is at most one "active" + glyph for any given section, the other glyphs are thus "inactive". +

+ +

+ While stand-alone (i.e. non-member) sections are displayed using a + set of 3 different colors, so that adjacent sections can be + differentiated, member-sections are colorized with the same color + imposed by their containing glyph. Usually, the glyph color is + determined by the glyph assigned shape. +

+ + diff --git a/src/main/omr/glyph/pattern/AlterPattern.java b/src/main/omr/glyph/pattern/AlterPattern.java new file mode 100644 index 0000000..d2046dd --- /dev/null +++ b/src/main/omr/glyph/pattern/AlterPattern.java @@ -0,0 +1,671 @@ +//----------------------------------------------------------------------------// +// // +// A l t e r P a t t e r n // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.glyph.pattern; + +import omr.constant.ConstantSet; + +import omr.glyph.CompoundBuilder; +import omr.glyph.Evaluation; +import omr.glyph.Glyphs; +import omr.glyph.Shape; +import omr.glyph.ShapeSet; +import omr.glyph.facets.Glyph; + +import omr.sheet.Scale; +import omr.sheet.SystemInfo; + +import omr.util.HorizontalSide; +import omr.util.Vip; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.awt.Rectangle; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.EnumSet; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.SortedSet; + +/** + * Class {@code AlterPattern} implements a pattern for alteration + * glyphs which have been "oversegmented" into stem(s) + other stuff. + *

This applies for sharp, natural and flat signs. + * We use the fact that the stem(s) are rather short and, for the case of sharp + * and natural, very close to each other. + * + * @author Hervé Bitteur + */ +public class AlterPattern + extends GlyphPattern +{ + //~ Static fields/initializers --------------------------------------------- + + /** Specific application parameters */ + private static final Constants constants = new Constants(); + + /** Usual logger utility */ + private static final Logger logger = LoggerFactory.getLogger(AlterPattern.class); + + //~ Instance fields -------------------------------------------------------- + // + // Scale-dependent constants for alter verification + private final int maxCloseStemDx; + + private final int minCloseStemOverlap; + + private final int maxAlterStemLength; + + private final int maxNaturalOverlap; + + private final int flatHeadWidth; + + private final int flatHeadHeight; + + // Adapters + private final PairAdapter sharpAdapter; + + private final PairAdapter naturalAdapter; + + /** Collection of (short) stems, sorted by abscissa. */ + private SortedSet stems; + + //~ Constructors ----------------------------------------------------------- + /** + * Creates a new AlterPattern object. + */ + public AlterPattern (SystemInfo system) + { + super("Alter", system); + + maxCloseStemDx = scale.toPixels(constants.maxCloseStemDx); + minCloseStemOverlap = scale.toPixels(constants.minCloseStemOverlap); + maxAlterStemLength = scale.toPixels(constants.maxAlterStemLength); + maxNaturalOverlap = scale.toPixels(constants.maxNaturalOverlap); + + flatHeadWidth = scale.toPixels(constants.flatHeadWidth); + flatHeadHeight = scale.toPixels(constants.flatHeadHeight); + + sharpAdapter = new SharpAdapter(system); + naturalAdapter = new NaturalAdapter(system); + } + + //~ Methods ---------------------------------------------------------------- + //------------// + // runPattern // + //------------// + /** + * Check the neighborhood of all short stems. + * + * @return the number of cases fixed + */ + @Override + public int runPattern () + { + int successNb = 0; // Success counter + + // Sorted short stems + stems = retrieveShortStems(); + + // Look for close stems (sharps & naturals) + successNb += checkCloseStems(); + + // Look for isolated stems (flats) + successNb += checkSingleStems(); + + // Impacted neighbors + checkFormerStems(); + + return successNb; + } + + //-----------------// + // checkCloseStems // + //-----------------// + /** + * Verify the case of stems very close to each other since they + * may result from oversegmentation of sharp or natural signs. + * + * @return the number of cases fixed + */ + private int checkCloseStems () + { + int nb = 0; + + for (Glyph glyph : stems) { + if (!glyph.isStem()) { + continue; // Already consumed + } + + // Retrieve interesting stem pairs in the neighborhood + List pairs = getNeighboringPairs(glyph); + + // Inspect pairs by increasing distance + for (StemPair pair : pairs) { + if (!pair.left.isStem() || !pair.right.isStem()) { + continue; // This pair is no longer relevant + } + + if (pair.isVip()) { + logger.info("{} Alter pair: {}", glyph.idString(), pair); + } + + // "hide" the stems to not perturb evaluation + pair.left.setShape(null); + pair.right.setShape(null); + + PairAdapter adapter; + + if (pair.overlap <= maxNaturalOverlap) { + logger.debug("NATURAL sign?"); + adapter = naturalAdapter; + } else { + logger.debug("SHARP sign?"); + adapter = sharpAdapter; + } + + // Prepare the adapter with proper stem boxes + adapter.setStemBoxes(pair.left.getBounds(), + pair.right.getBounds()); + + Glyph compound = system.buildCompound( + pair.left, + true, + system.getGlyphs(), + adapter); + + if (compound != null) { + nb++; + logger.debug("{}Compound #{} rebuilt as {}", + system.getLogPrefix(), + compound.getId(), compound.getShape()); + } else { + // Restore stem shapes + pair.left.setShape(Shape.STEM); + pair.right.setShape(Shape.STEM); + } + } + } + + return nb; + } + + //------------------// + // checkFormerStems // + //------------------// + /** + * Look for glyphs whose shape was dependent on former stems, + * and call their shape into question again. + */ + private void checkFormerStems () + { + SortedSet impacted = Glyphs.sortedSet(); + + for (Glyph glyph : system.getGlyphs()) { + if (!glyph.isActive()) { + continue; + } + + for (HorizontalSide side : HorizontalSide.values()) { + // Retrieve "deassigned" stem if any + Glyph stem = glyph.getStem(side); + + if ((stem != null) && (stem.getShape() != Shape.STEM)) { + impacted.add(glyph); + } + } + } + + if (logger.isDebugEnabled()) { + logger.debug( + Glyphs.toString("Impacted alteration neighbors", impacted)); + } + + for (Glyph glyph : impacted) { + Shape shape = glyph.getShape(); + + if (ShapeSet.StemSymbols.contains(shape)) { + // Trigger a reevaluation (w/o forbidding the current shape) + glyph.setShape(null); + glyph.allowShape(shape); + } + + // Re-compute glyph features + system.computeGlyphFeatures(glyph); + } + } + + //------------------// + // checkSingleStems // + //------------------// + /** + * Verify the case of isolated short stems since they may result + * from oversegmentation of flat signs. + * + * @return the number of cases fixed + */ + private int checkSingleStems () + { + int nb = 0; + FlatAdapter flatAdapter = new FlatAdapter(system); + + for (Glyph glyph : stems) { + if (!glyph.isStem()) { + continue; + } + + // If stem already has notehead or flag/beam, skip it + Set goods = new HashSet<>(); + Set bads = new HashSet<>(); + glyph.getSymbolsBefore(StemPattern.reliableStemSymbols, goods, bads); + glyph.getSymbolsAfter(StemPattern.reliableStemSymbols, goods, bads); + if (!goods.isEmpty()) { + logger.debug("Skipping good stem {}", glyph); + continue; + } + + // "hide" the stems temporarily to not perturb evaluation + glyph.setShape(null); + + Glyph compound = system.buildCompound( + glyph, + true, + system.getGlyphs(), + flatAdapter); + + if (compound != null) { + nb++; + + logger.debug("{}Compound #{} rebuilt as {}", + system.getLogPrefix(), + compound.getId(), compound.getShape()); + } else { + // Restore stem shape + glyph.setShape(Shape.STEM); + } + } + + return nb; + } + + //--------------------// + // retrieveShortStems // + //--------------------// + /** + * Retrieve the collection of all stems in the system, + * ordered naturally by their abscissa. + * + * @return the set of short stems + */ + private SortedSet retrieveShortStems () + { + final SortedSet shortStems = Glyphs.sortedSet(); + + for (Glyph glyph : system.getGlyphs()) { + if (glyph.isStem() && glyph.isActive()) { + // Check stem length + if (glyph.getBounds().height <= maxAlterStemLength) { + shortStems.add(glyph); + } + } + } + + return shortStems; + } + + //---------------------// + // getNeighboringPairs // + //---------------------// + /** + * Retrieve all pairs of stems, transitively close to the provided + * seed, to pickup the most promising pair for natural/sharp alter. + * + * @param seed the stem seed + * @return the collection of stems pairs + */ + private List getNeighboringPairs (Glyph seed) + { + List pairs = new ArrayList<>(); + + // First, come up with candidate stems + SortedSet neighbors = Glyphs.sortedSet(Arrays.asList(seed)); + Rectangle box = seed.getBounds(); + + for (Glyph glyph : stems) { + if (glyph != seed && glyph.isStem()) { + Rectangle glyphBox = glyph.getBounds(); + glyphBox.grow(maxCloseStemDx, 0); + + if (box.intersects(glyphBox)) { + neighbors.add(glyph); + box.add(glyph.getBounds()); + } else if (glyphBox.x > box.x + box.width) { + break; + } + } + } + + // Second, evaluate pairs and keep only the possible ones + for (Glyph left : neighbors) { + final Rectangle leftBox = left.getBounds(); + final int leftX = leftBox.x + (leftBox.width / 2); + + for (Glyph other : neighbors.tailSet(left)) { + if ((other == left)) { + continue; + } + + // Check horizontal distance + final Rectangle rightBox = other.getBounds(); + final int rightX = rightBox.x + (rightBox.width / 2); + if (rightX - leftX > maxCloseStemDx) { + continue; + } + + // Check vertical overlap + final int commonTop = Math.max(leftBox.y, rightBox.y); + final int commonBot = Math.min( + leftBox.y + leftBox.height, + rightBox.y + rightBox.height); + final int overlap = commonBot - commonTop; + + if (overlap < minCloseStemOverlap) { + continue; + } + + // Evaluate compatibility + double deltaLength = Math.abs(leftBox.height - rightBox.height); + double deltaRatio = deltaLength + / Math.max(leftBox.height, rightBox.height); + pairs.add(new StemPair(left, other, overlap, deltaRatio)); + } + } + + Collections.sort(pairs); + return pairs; + } + + //~ Inner Classes ---------------------------------------------------------- + //-----------// + // Constants // + //-----------// + private static final class Constants + extends ConstantSet + { + //~ Instance fields ---------------------------------------------------- + + Evaluation.Grade alterMinGrade = new Evaluation.Grade( + 0.3, + "Minimum grade for sharp/natural sign verification"); + + Evaluation.Grade flatMinGrade = new Evaluation.Grade( + 20d, + "Minimum grade for flat sign verification"); + + Scale.Fraction maxCloseStemDx = new Scale.Fraction( + 0.7d, + "Maximum horizontal distance for close stems"); + + Scale.Fraction maxAlterStemLength = new Scale.Fraction( + 3.5, + "Maximum length for pseudo-stem(s) in alteration sign"); + + Scale.Fraction maxNaturalOverlap = new Scale.Fraction( + 2.0d, + "Maximum vertical overlap for natural stems"); + + Scale.Fraction minCloseStemOverlap = new Scale.Fraction( + 0.5d, + "Minimum vertical overlap for close stems"); + + Scale.Fraction flatHeadHeight = new Scale.Fraction( + 1d, + "Typical height of flat head"); + + Scale.Fraction flatHeadWidth = new Scale.Fraction( + 0.5d, + "Typical width of flat head"); + + } + + //-------------// + // FlatAdapter // + //-------------// + /** + * Compound adapter meant to build flats. + */ + private class FlatAdapter + extends CompoundBuilder.TopShapeAdapter + { + //~ Constructors ------------------------------------------------------- + + public FlatAdapter (SystemInfo system) + { + super( + system, + constants.flatMinGrade.getValue(), + EnumSet.of(Shape.FLAT)); + } + + //~ Methods ------------------------------------------------------------ + @Override + public Rectangle computeReferenceBox () + { + final Rectangle stemBox = seed.getBounds(); + + return new Rectangle( + stemBox.x, + (stemBox.y + stemBox.height) - flatHeadHeight, + flatHeadWidth, + flatHeadHeight); + } + + @Override + public boolean isCandidateSuitable (Glyph glyph) + { + + if (glyph.isManualShape()) { + return false; + } + + Shape shape = glyph.getShape(); + + return !ShapeSet.StemSymbols.contains(shape) + || shape == Shape.BEAM_HOOK; + } + } + + //----------------// + // NaturalAdapter // + //----------------// + /** + * Compound adapter meant to build naturals. + */ + private class NaturalAdapter + extends PairAdapter + { + //~ Constructors ------------------------------------------------------- + + public NaturalAdapter (SystemInfo system) + { + super(system, EnumSet.of(Shape.NATURAL)); + } + + //~ Methods ------------------------------------------------------------ + @Override + public Rectangle computeReferenceBox () + { + Rectangle newBox = getStemsBox(); + newBox.grow(maxCloseStemDx / 4, minCloseStemOverlap / 2); + + return newBox; + } + } + + //-------------// + // PairAdapter // + //-------------// + /** + * Abstract compound adapter meant to build sharps or naturals + * from a pair of close stems. + */ + private abstract class PairAdapter + extends CompoundBuilder.TopShapeAdapter + { + //~ Instance fields ---------------------------------------------------- + + protected Rectangle leftBox; + + protected Rectangle rightBox; + + //~ Constructors ------------------------------------------------------- + public PairAdapter (SystemInfo system, + EnumSet shapes) + { + super(system, constants.alterMinGrade.getValue(), shapes); + } + + //~ Methods ------------------------------------------------------------ + @Override + public boolean isCandidateClose (Glyph glyph) + { + // We use containment instead of intersection + return box.contains(glyph.getBounds()); + } + + @Override + public boolean isCandidateSuitable (Glyph glyph) + { + return !glyph.isManualShape(); + } + + public void setStemBoxes (Rectangle leftBox, + Rectangle rightBox) + { + this.leftBox = leftBox; + this.rightBox = rightBox; + } + + protected Rectangle getStemsBox () + { + if ((leftBox == null) || (rightBox == null)) { + throw new NullPointerException("Stem boxes have not been set"); + } + + Rectangle box = new Rectangle(leftBox); + box.add(rightBox); + + return box; + } + } + + //--------------// + // SharpAdapter // + //--------------// + /** + * Compound adapter meant to build sharps. + */ + private class SharpAdapter + extends PairAdapter + { + //~ Constructors ------------------------------------------------------- + + public SharpAdapter (SystemInfo system) + { + super(system, EnumSet.of(Shape.SHARP)); + } + + //~ Methods ------------------------------------------------------------ + @Override + public Rectangle computeReferenceBox () + { + Rectangle newBox = getStemsBox(); + newBox.grow(maxCloseStemDx / 2, minCloseStemOverlap / 2); + + return newBox; + } + } + + //----------// + // StemPair // + //----------// + /** + * Data about a possible pair of stems for a sharp/natural alter. + */ + private class StemPair + implements Comparable, Vip + { + + /** Stem on left side. */ + final Glyph left; + + /** Stem on right side. */ + final Glyph right; + + /** Vertical overlap. */ + final int overlap; + + /** Info about pair "distance" (to pick the best pair). */ + final double distance; + + boolean vip = false; + + public StemPair (Glyph left, + Glyph right, + int overlap, + double distance) + { + this.left = left; + this.right = right; + this.overlap = overlap; + this.distance = distance; + + if (left.isVip() || right.isVip()) { + setVip(); + } + } + + /** To sort pairs. */ + @Override + public int compareTo (StemPair that) + { + return Double.compare(this.distance, that.distance); + } + + @Override + public String toString () + { + StringBuilder sb = new StringBuilder("{Stems"); + sb.append(" #").append(left.getId()); + sb.append(" #").append(right.getId()); + sb.append(" over:").append(overlap); + sb.append(" dist:").append((float) distance); + sb.append("}"); + return sb.toString(); + } + + @Override + public final boolean isVip () + { + return vip; + } + + @Override + public final void setVip () + { + this.vip = true; + } + } +} diff --git a/src/main/omr/glyph/pattern/ArticulationPattern.java b/src/main/omr/glyph/pattern/ArticulationPattern.java new file mode 100644 index 0000000..c25ef69 --- /dev/null +++ b/src/main/omr/glyph/pattern/ArticulationPattern.java @@ -0,0 +1,141 @@ +//----------------------------------------------------------------------------// +// // +// A r t i c u l a t i o n P a t t e r n // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.glyph.pattern; + +import omr.constant.ConstantSet; + +import omr.glyph.Evaluation; +import omr.glyph.Glyphs; +import omr.glyph.Shape; +import omr.glyph.ShapeSet; +import omr.glyph.facets.Glyph; + +import omr.grid.StaffInfo; + +import omr.sheet.NotePosition; +import omr.sheet.Scale; +import omr.sheet.SystemInfo; + +import omr.util.Predicate; +import omr.util.VerticalSide; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.awt.Point; +import java.awt.Rectangle; +import java.util.List; + +/** + * Class {@code ArticulationPattern} verifies that any articulation + * glyph has corresponding note(s) below or above in the staff. + * + * @author Hervé Bitteur + */ +public class ArticulationPattern + extends GlyphPattern +{ + //~ Static fields/initializers --------------------------------------------- + + /** Specific application parameters */ + private static final Constants constants = new Constants(); + + /** Usual logger utility */ + private static final Logger logger = LoggerFactory.getLogger( + ArticulationPattern.class); + + //~ Constructors ----------------------------------------------------------- + //---------------------// + // ArticulationPattern // + //---------------------// + /** + * Creates a new ArticulationPattern object. + * + * @param system the system to process + */ + public ArticulationPattern (SystemInfo system) + { + super("Articulation", system); + } + + //~ Methods ---------------------------------------------------------------- + //------------// + // runPattern // + //------------// + @Override + public int runPattern () + { + int xMargin = system.getSheet() + .getScale() + .toPixels(constants.xMargin); + int nb = 0; + + for (Glyph glyph : system.getGlyphs()) { + if (!ShapeSet.Articulations.contains(glyph.getShape()) + || glyph.isManualShape()) { + continue; + } + + Point center = glyph.getAreaCenter(); + NotePosition pos = system.getNoteStaffAt(center); + StaffInfo staff = pos.getStaff(); + Rectangle box = glyph.getBounds(); + + // Extend height till end of staff area + double topLimit = staff.getLimitAtX(VerticalSide.TOP, center.x); + double botLimit = staff.getLimitAtX(VerticalSide.BOTTOM, center.x); + box.y = (int) Math.rint(topLimit); + box.height = (int) Math.rint(botLimit) - box.y; + box.grow(xMargin, 0); + + List glyphs = system.lookupIntersectedGlyphs(box, glyph); + boolean hasNote = Glyphs.contains( + glyphs, + new Predicate() + { + @Override + public boolean check (Glyph entity) + { + Shape shape = entity.getShape(); + + return ShapeSet.NoteHeads.contains(shape) + || ShapeSet.Notes.contains(shape) + || ShapeSet.Rests.contains(shape); + } + }); + + if (!hasNote) { + logger.debug("Deassign articulation {}", glyph.idString()); + + glyph.setShape(null, Evaluation.ALGORITHM); + nb++; + } + } + + return nb; + } + + //~ Inner Classes ---------------------------------------------------------- + //-----------// + // Constants // + //-----------// + private static final class Constants + extends ConstantSet + { + //~ Instance fields ---------------------------------------------------- + + Scale.Fraction xMargin = new Scale.Fraction( + 0.5, + "Abscissa margin around articulation"); + + } +} diff --git a/src/main/omr/glyph/pattern/BassPattern.java b/src/main/omr/glyph/pattern/BassPattern.java new file mode 100644 index 0000000..36588ff --- /dev/null +++ b/src/main/omr/glyph/pattern/BassPattern.java @@ -0,0 +1,193 @@ +//----------------------------------------------------------------------------// +// // +// B a s s P a t t e r n // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.glyph.pattern; + +import omr.constant.Constant; +import omr.constant.ConstantSet; + +import omr.glyph.CompoundBuilder; +import omr.glyph.CompoundBuilder.CompoundAdapter; +import omr.glyph.Grades; +import omr.glyph.Shape; +import omr.glyph.ShapeSet; +import omr.glyph.facets.Glyph; + +import omr.grid.StaffInfo; + +import omr.sheet.Scale; +import omr.sheet.SystemInfo; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.awt.Point; +import java.awt.Rectangle; + +/** + * Class {@code BassPattern} checks for segmented bass clefs, in the + * neighborhood of typical vertical two-dot patterns + * + * @author Hervé Bitteur + */ +public class BassPattern + extends GlyphPattern +{ + //~ Static fields/initializers --------------------------------------------- + + /** Specific application parameters */ + private static final Constants constants = new Constants(); + + /** Usual logger utility */ + private static final Logger logger = LoggerFactory.getLogger( + BassPattern.class); + + //~ Constructors ----------------------------------------------------------- + //-------------// + // BassPattern // + //-------------// + /** + * Creates a new BassPattern object. + * + * @param system the containing system + */ + public BassPattern (SystemInfo system) + { + super("Bass", system); + } + + //~ Methods ---------------------------------------------------------------- + //------------// + // runPattern // + //------------// + @Override + public int runPattern () + { + int successNb = 0; + + // Constants for clef verification + final double maxBassDotPitchDy = constants.maxBassDotPitchDy.getValue(); + final double maxBassDotDx = scale.toPixels(constants.maxBassDotDx); + + // Specific adapter definition for bass clefs + CompoundAdapter bassAdapter = new BassAdapter( + system, + Grades.clefMinGrade); + + for (Glyph top : system.getGlyphs()) { + // Look for top dot + if ((top.getShape() != Shape.DOT_set) + || (Math.abs(top.getPitchPosition() - -3) > maxBassDotPitchDy)) { + continue; + } + + int topX = top.getCentroid().x; + StaffInfo topStaff = system.getStaffAt(top.getCentroid()); + + // Look for bottom dot right underneath, and in the same staff + for (Glyph bot : system.getGlyphs()) { + if ((bot.getShape() != Shape.DOT_set) + || (Math.abs(bot.getPitchPosition() - -1) > maxBassDotPitchDy)) { + continue; + } + + if (Math.abs(bot.getCentroid().x - topX) > maxBassDotDx) { + continue; + } + + if (system.getStaffAt(bot.getCentroid()) != topStaff) { + continue; + } + + // Here we have a couple + logger.debug( + "Got bass dots #{} & #{}", + top.getId(), + bot.getId()); + + Glyph compound = system.buildCompound( + top, + true, + system.getGlyphs(), + bassAdapter); + + if (compound != null) { + successNb++; + } + } + } + + return successNb; + } + + //~ Inner Classes ---------------------------------------------------------- + //-----------// + // Constants // + //-----------// + private static final class Constants + extends ConstantSet + { + //~ Instance fields ---------------------------------------------------- + + Scale.Fraction maxBassDotDx = new Scale.Fraction( + 0.25, + "Tolerance on Bass dot abscissae"); + + Constant.Double maxBassDotPitchDy = new Constant.Double( + "pitch", + 0.5, + "Ordinate tolerance on a Bass dot pitch position"); + + } + + //-------------// + // BassAdapter // + //-------------// + /** + * This is the compound adapter meant to build bass clefs + */ + private class BassAdapter + extends CompoundBuilder.TopShapeAdapter + { + //~ Constructors ------------------------------------------------------- + + public BassAdapter (SystemInfo system, + double minGrade) + { + super(system, minGrade, ShapeSet.BassClefs); + } + + //~ Methods ------------------------------------------------------------ + @Override + public Rectangle computeReferenceBox () + { + if (seed == null) { + throw new NullPointerException( + "Compound seed has not been set"); + } + + Rectangle pixRect = new Rectangle(seed.getCentroid()); + pixRect.add( + new Point( + pixRect.x - (2 * scale.getInterline()), + pixRect.y + (3 * scale.getInterline()))); + + return pixRect; + } + + @Override + public boolean isCandidateSuitable (Glyph glyph) + { + return !glyph.isManualShape() + || ShapeSet.BassClefs.contains(glyph.getShape()); + } + } +} diff --git a/src/main/omr/glyph/pattern/BeamHookPattern.java b/src/main/omr/glyph/pattern/BeamHookPattern.java new file mode 100644 index 0000000..62cd140 --- /dev/null +++ b/src/main/omr/glyph/pattern/BeamHookPattern.java @@ -0,0 +1,141 @@ +//----------------------------------------------------------------------------// +// // +// B e a m H o o k P a t t e r n // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.glyph.pattern; + +import omr.glyph.Shape; +import omr.glyph.ShapeSet; +import omr.glyph.facets.Glyph; + +import omr.sheet.SystemInfo; + +import omr.util.HorizontalSide; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.awt.Rectangle; + +/** + * Class {@code BeamHookPattern} removes beam hooks for which the + * related stem has no beam on the same horizontal side of the stem + * and on the same vertical end of the stem. + * + * @author Hervé Bitteur + */ +public class BeamHookPattern + extends GlyphPattern +{ + //~ Static fields/initializers --------------------------------------------- + + /** Usual logger utility */ + private static final Logger logger = LoggerFactory.getLogger( + BeamHookPattern.class); + + //~ Constructors ----------------------------------------------------------- + //-----------------// + // BeamHookPattern // + //-----------------// + /** + * Creates a new BeamHookPattern object. + * + * @param system the system to process + */ + public BeamHookPattern (SystemInfo system) + { + super("BeamHook", system); + } + + //~ Methods ---------------------------------------------------------------- + //------------// + // runPattern // + //------------// + @Override + public int runPattern () + { + int nb = 0; + + for (Glyph hook : system.getGlyphs()) { + if ((hook.getShape() != Shape.BEAM_HOOK) || hook.isManualShape()) { + continue; + } + + if (hook.getStemNumber() != 1) { + if (hook.isVip() || logger.isDebugEnabled()) { + logger.info( + "{} stem(s) for beam hook #{}", + hook.getStemNumber(), + hook.getId()); + } + + hook.setShape(null); + nb++; + } else { + if (hook.isVip() || logger.isDebugEnabled()) { + logger.info("Checking hook #{}", hook.getId()); + } + + Glyph stem = null; + HorizontalSide side = null; + + for (HorizontalSide s : HorizontalSide.values()) { + side = s; + stem = hook.getStem(s); + + if (stem != null) { + break; + } + } + + int hookDy = hook.getCentroid().y - stem.getCentroid().y; + + // Look for other stuff on the stem + Rectangle stemBox = system.stemBoxOf(stem); + boolean beamFound = false; + + for (Glyph g : system.lookupIntersectedGlyphs( + stemBox, + stem, + hook)) { + // We look for a beam on the same stem side + if ((g.getStem(side) == stem)) { + Shape shape = g.getShape(); + + if (ShapeSet.Beams.contains(shape) + && (shape != Shape.BEAM_HOOK)) { + if (hook.isVip() || logger.isDebugEnabled()) { + logger.info( + "Confirmed beam hook #{}", + hook.getId()); + } + + beamFound = true; + + break; + } + } + } + + if (!beamFound) { + // Deassign this hook w/ no beam neighbor + if (hook.isVip() || logger.isDebugEnabled()) { + logger.info("Cancelled beam hook #{}", hook.getId()); + } + + hook.setShape(null); + nb++; + } + } + } + + return nb; + } +} diff --git a/src/main/omr/glyph/pattern/CaesuraPattern.java b/src/main/omr/glyph/pattern/CaesuraPattern.java new file mode 100644 index 0000000..52bc469 --- /dev/null +++ b/src/main/omr/glyph/pattern/CaesuraPattern.java @@ -0,0 +1,89 @@ +//----------------------------------------------------------------------------// +// // +// C a e s u r a P a t t e r n // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.glyph.pattern; + +import omr.glyph.Shape; +import omr.glyph.facets.Glyph; + +import omr.score.entity.Measure; +import omr.score.entity.ScoreSystem; +import omr.score.entity.SystemPart; + +import omr.sheet.SystemInfo; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.awt.Point; + +/** + * Class {@code CaesuraPattern} checks that a caesura in a measure + * is not surrounded with chords. + * + * @author Hervé Bitteur + */ +public class CaesuraPattern + extends GlyphPattern +{ + //~ Static fields/initializers --------------------------------------------- + + /** Usual logger utility */ + private static final Logger logger = LoggerFactory.getLogger( + CaesuraPattern.class); + + //~ Constructors ----------------------------------------------------------- + //----------------// + // CaesuraPattern // + //----------------// + /** + * Creates a new CaesuraPattern object. + * + * @param system the system to process + */ + public CaesuraPattern (SystemInfo system) + { + super("Caesura", system); + } + + //~ Methods ---------------------------------------------------------------- + //------------// + // runPattern // + //------------// + @Override + public int runPattern () + { + int nb = 0; + ScoreSystem scoreSystem = system.getScoreSystem(); + + for (Glyph glyph : system.getGlyphs()) { + if ((glyph.getShape() != Shape.CAESURA) || glyph.isManualShape()) { + continue; + } + + Point center = glyph.getAreaCenter(); + SystemPart part = scoreSystem.getPartAt(center); + Measure measure = part.getMeasureAt(center); + + if (!measure.getChords() + .isEmpty()) { + if (glyph.isVip() || logger.isDebugEnabled()) { + logger.info("Cancelled caesura #{}", glyph.getId()); + } + + glyph.setShape(null); + nb++; + } + } + + return nb; + } +} diff --git a/src/main/omr/glyph/pattern/ClefPattern.java b/src/main/omr/glyph/pattern/ClefPattern.java new file mode 100644 index 0000000..3fa057a --- /dev/null +++ b/src/main/omr/glyph/pattern/ClefPattern.java @@ -0,0 +1,322 @@ +//----------------------------------------------------------------------------// +// // +// C l e f P a t t e r n // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.glyph.pattern; + +import omr.constant.ConstantSet; + +import omr.glyph.Evaluation; +import omr.glyph.GlyphNetwork; +import omr.glyph.Glyphs; +import omr.glyph.Grades; +import omr.glyph.Nest; +import omr.glyph.Shape; +import omr.glyph.ShapeSet; +import omr.glyph.facets.Glyph; + +import omr.grid.StaffInfo; + +import omr.sheet.Scale; +import omr.sheet.SystemInfo; + +import omr.util.HorizontalSide; +import omr.util.Predicate; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.awt.Rectangle; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** + * Class {@code ClefPattern} verifies all the initial clefs of a + * system, using an intersection inner rectangle and a containing + * outer rectangle to retrieve the clef glyphs and only those ones. + * + * @author Hervé Bitteur + */ +public class ClefPattern + extends GlyphPattern +{ + //~ Static fields/initializers --------------------------------------------- + + /** Specific application parameters */ + private static final Constants constants = new Constants(); + + /** Usual logger utility */ + private static final Logger logger = LoggerFactory.getLogger(ClefPattern.class); + + /** Specific predicate to filter clef shapes */ + private static final Predicate clefShapePredicate = new Predicate() + { + @Override + public boolean check (Shape shape) + { + return ShapeSet.Clefs.contains(shape); + } + }; + + /** Specific predicate to filter clef glyphs */ + private static final Predicate clefGlyphPredicate = new Predicate() + { + @Override + public boolean check (Glyph glyph) + { + return glyph.isClef(); + } + }; + + //~ Instance fields -------------------------------------------------------- + /** Glyphs nest */ + private final Nest nest; + + // Scale-dependent parameters + private final int clefWidth; + + private final int xOffset; + + private final int yOffset; + + private final int xMargin; + + private final int yMargin; + + //~ Constructors ----------------------------------------------------------- + /** + * Creates a new ClefPattern object. + * + * @param system the containing system + */ + public ClefPattern (SystemInfo system) + { + super("Clef", system); + + nest = system.getSheet().getNest(); + + clefWidth = scale.toPixels(constants.clefWidth); + xOffset = scale.toPixels(constants.xOffset); + yOffset = scale.toPixels(constants.yOffset); + xMargin = scale.toPixels(constants.xMargin); + yMargin = scale.toPixels(constants.yMargin); + } + + //~ Methods ---------------------------------------------------------------- + //------------// + // runPattern // + //------------// + /** + * Check that each staff begins with a clef. + * + * @return the number of clefs rebuilt + */ + @Override + public int runPattern () + { + int successNb = 0; + int staffId = 0; + + for (StaffInfo staff : system.getStaves()) { + staffId++; + + // Define the inner box to intersect clef glyph(s) + int left = (int) Math.rint( + staff.getAbscissa(HorizontalSide.LEFT)); + Rectangle inner = new Rectangle( + left + (2 * xOffset) + (clefWidth / 2), + staff.getFirstLine().yAt(left) + (staff.getHeight() / 2), + 0, + 0); + inner.grow( + (clefWidth / 2) - xOffset, + (staff.getHeight() / 2) - yOffset); + + // Remember the box, for visual debug + staff.addAttachment(" ci", inner); + + // We must find a clef out of these glyphs + Collection glyphs = system.lookupIntersectedGlyphs(inner); + logger.debug("{}{}", staffId, Glyphs.toString(" int", glyphs)); + + // We assume than there can't be any alien among them, so we should + // rebuild the larger glyph which the alien had wrongly segmented + Set impacted = new HashSet<>(); + + for (Glyph glyph : glyphs) { + if (glyph.getShape() == Shape.STEM) { + logger.debug("Clef: Removed stem#{}", glyph.getId()); + + impacted.addAll(glyph.getConnectedNeighbors()); + impacted.add(glyph); + } + } + + if (!impacted.isEmpty()) { + // Rebuild the larger glyph + Glyph larger = system.buildCompound(impacted); + if (larger != null) { + logger.debug("Rebuilt stem-segmented {}", larger.idString()); + } + + // Recompute the set of intersected glyphs + glyphs = system.lookupIntersectedGlyphs(inner); + } + + if (checkClef(glyphs, staff)) { + successNb++; + } + } + + return successNb; + } + + //-----------// + // checkClef // + //-----------// + /** + * Try to recognize a clef in the compound of the provided glyphs. + * + * @param glyphs the parts of a clef candidate + * @param staff the containing staff + * @return true if successful + */ + private boolean checkClef (Collection glyphs, + StaffInfo staff) + { + if (glyphs.isEmpty()) { + return false; + } + + // Check if we already have a clef among the intersected glyphs + Set clefs = Glyphs.lookupGlyphs(glyphs, clefGlyphPredicate); + Glyph orgClef = null; + + if (!clefs.isEmpty()) { + if (Glyphs.containsManual(clefs)) { + return false; // Respect user decision + } else { + // Remember grade of the best existing clef + for (Glyph glyph : clefs) { + if ((orgClef == null) + || (glyph.getGrade() > orgClef.getGrade())) { + orgClef = glyph; + } + } + } + } + + // Remove potential aliens + Glyphs.purgeManuals(glyphs); + + Glyph compound = system.buildTransientCompound(glyphs); + + // Check if a clef appears in the top evaluations + Evaluation vote = GlyphNetwork.getInstance().vote( + compound, + system, + Grades.clefMinGrade, + clefShapePredicate); + + if ((vote != null) + && ((orgClef == null) || (vote.grade > orgClef.getGrade()))) { + // We now have a clef! + // Look around for an even better result... + logger.debug("{} built from {}", + vote.shape, Glyphs.toString(glyphs)); + + // Look for larger stuff + Rectangle outer = compound.getBounds(); + outer.grow(xMargin, yMargin); + + // Remember the box, for visual debug + staff.addAttachment("co", outer); + + List outerGlyphs = system.lookupIntersectedGlyphs(outer); + outerGlyphs.removeAll(glyphs); + Collections.sort(outerGlyphs, Glyph.byReverseWeight); + + final double minWeight = constants.minWeight.getValue(); + + for (Glyph g : outerGlyphs) { + // Consider only glyphs with a minimum weight + if (g.getNormalizedWeight() < minWeight) { + break; + } + + logger.debug("Considering {}", g); + + Glyph newCompound = system.buildTransientCompound( + Arrays.asList(compound, g)); + final Evaluation newVote = GlyphNetwork.getInstance().vote( + newCompound, + system, + Grades.clefMinGrade, + clefShapePredicate); + + if ((newVote != null) && (newVote.grade > vote.grade)) { + logger.debug("{} better built with {}", vote, g.idString()); + + compound = newCompound; + vote = newVote; + } + } + + // Register the last definition of the clef + compound = system.addGlyph(compound); + compound.setShape(vote.shape, Evaluation.ALGORITHM); + + logger.debug("{} rebuilt as {}", vote.shape, compound.idString()); + + return true; + } else { + return false; + } + } + + //~ Inner Classes ---------------------------------------------------------- + //-----------// + // Constants // + //-----------// + private static final class Constants + extends ConstantSet + { + //~ Instance fields ---------------------------------------------------- + + Scale.Fraction clefWidth = new Scale.Fraction( + 3d, + "Width of a clef"); + + Scale.Fraction xOffset = new Scale.Fraction( + 0.2d, + "Clef horizontal offset since left bar"); + + Scale.Fraction yOffset = new Scale.Fraction( + 0d, + "Clef vertical offset since staff line"); + + Scale.Fraction xMargin = new Scale.Fraction( + 0d, + "Clef horizontal outer margin"); + + Scale.Fraction yMargin = new Scale.Fraction( + 0.5d, + "Clef vertical outer margin"); + + Scale.AreaFraction minWeight = new Scale.AreaFraction( + 0.1, + "Minimum normalized weight to be added to a clef"); + + } +} diff --git a/src/main/omr/glyph/pattern/DotPattern.java b/src/main/omr/glyph/pattern/DotPattern.java new file mode 100644 index 0000000..0644e1a --- /dev/null +++ b/src/main/omr/glyph/pattern/DotPattern.java @@ -0,0 +1,254 @@ +//----------------------------------------------------------------------------// +// // +// D o t P a t t e r n // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.glyph.pattern; + +import omr.constant.Constant; +import omr.constant.ConstantSet; + +import omr.glyph.Glyphs; +import omr.glyph.Shape; +import omr.glyph.ShapeSet; +import omr.glyph.facets.Glyph; + +import omr.run.Orientation; + +import omr.sheet.Scale; +import omr.sheet.SystemInfo; + +import omr.text.TextLine; +import omr.text.TextWord; + +import omr.util.Predicate; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.awt.Rectangle; +import java.awt.geom.Line2D; +import java.util.Collections; +import java.util.Set; + +/** + * Class {@code DotPattern} filters the dot glyphs to remove those + * which are actually text dashes ('-') within sentences. + * + * @author Hervé Bitteur + */ +public class DotPattern + extends GlyphPattern +{ + //~ Static fields/initializers --------------------------------------------- + + /** Specific application parameters */ + private static final Constants constants = new Constants(); + + /** Usual logger utility */ + private static final Logger logger = LoggerFactory.getLogger( + DotPattern.class); + + //~ Instance fields -------------------------------------------------------- + // + /** Max dx from sentence end to dot. */ + private final int maxLineDx; + + /** Max dy from sentence baseline to dot. */ + private final int maxLineDy; + + //~ Constructors ----------------------------------------------------------- + // + //------------// + // DotPattern // + //------------// + /** + * Creates a new DotPattern object. + * + * @param system the dedicated system + */ + public DotPattern (SystemInfo system) + { + super("Dot", system); + + // Scale-dependent parameters + maxLineDx = scale.toPixels(constants.maxLineDx); + maxLineDy = scale.toPixels(constants.maxLineDy); + } + + //~ Methods ---------------------------------------------------------------- + // + //------------// + // runPattern // + //------------// + /** + * In a specified system, look for all dots that should not be kept. + * + * @return the number of dots deassigned + */ + @Override + public int runPattern () + { + int nb = 0; + String language = system.getSheet() + .getPage() + .getTextParam() + .getTarget(); + + for (Glyph glyph : getQuestionableDots()) { + // Check alignment with a TextLine + TextLine line = getEmbracingLine(glyph); + + if (line == null) { + continue; + } + + // Check shape + if (!isDashLooking(glyph)) { + continue; + } + + // OK, assign it the character shape + glyph.setShape(Shape.CHARACTER); + + // Insert it into line + TextWord word = TextWord.createManualWord(glyph, "-"); + glyph.setTextWord(language, word); + line.addWords(Collections.singleton(word)); + logger.debug("Reassign dot#{} to {}", glyph.getId(), line); + + // Counters + nb++; + } + + return nb; + } + + //------------------// + // getEmbracingLine // + //------------------// + /** + * Lookup for a TextLine that embraces the provided glyph. + * + * @param the (dot) glyph to check + * @return glyph the embracing line if any, otherwise null + */ + private TextLine getEmbracingLine (Glyph glyph) + { + Rectangle glyphBox = glyph.getBounds(); + + for (TextLine sentence : system.getSentences()) { + Line2D baseline = sentence.getBaseline(); + + // Check in abscissa: not before sentence beginning + if ((glyphBox.x + glyphBox.width) <= baseline.getX1()) { + continue; + } + + // Check in abscissa: not too far after sentence end + if ((glyphBox.x - baseline.getX2()) > maxLineDx) { + continue; + } + + // Check in abscissa: not overlapping any sentence word + for (TextWord word : sentence.getWords()) { + if (word.getBounds() + .intersects(glyphBox)) { + continue; + } + } + + // Check in ordinate: distance from baseline + double dy = baseline.ptLineDist(glyph.getAreaCenter()); + + if (dy > maxLineDy) { + continue; + } + + // This line is OK, take it + return sentence; + } + + // Nothing found + return null; + } + + //---------------------// + // getQuestionableDots // + //---------------------// + /** + * Retrieve questionable dots. + * + * @return the set of dots to further check + */ + private Set getQuestionableDots () + { + Set dots = Glyphs.lookupGlyphs( + system.getGlyphs(), + new Predicate() + { + @Override + public boolean check (Glyph glyph) + { + Shape shape = glyph.getShape(); + + return (shape != null) + && ShapeSet.Dots.contains(shape) + && !glyph.isManualShape() && glyph.isActive(); + } + }); + + if (logger.isDebugEnabled()) { + logger.debug( + "{} Questionable {}", + system.getLogPrefix(), + Glyphs.toString("dots", dots)); + } + + return dots; + } + + //---------------// + // isDashLooking // + //---------------// + /** + * Check whether the provided glyph looks like a '-' character. + * + * @param glyph the glyph to check + * @return true if tests are OK + */ + private boolean isDashLooking (Glyph glyph) + { + return glyph.getAspect(Orientation.HORIZONTAL) >= constants.minAspect.getValue(); + } + + //~ Inner Classes ---------------------------------------------------------- + // + //-----------// + // Constants // + //-----------// + private static final class Constants + extends ConstantSet + { + //~ Instance fields ---------------------------------------------------- + + Constant.Ratio minAspect = new Constant.Ratio( + 2, + "Minimum width / height ratio for a dash"); + + Scale.Fraction maxLineDx = new Scale.Fraction( + 10, + "Maximum abscissa offset from line to dash"); + + Scale.Fraction maxLineDy = new Scale.Fraction( + 2, + "Maximum ordinate offset from line to dash"); + + } +} diff --git a/src/main/omr/glyph/pattern/DoubleBeamPattern.java b/src/main/omr/glyph/pattern/DoubleBeamPattern.java new file mode 100644 index 0000000..375226b --- /dev/null +++ b/src/main/omr/glyph/pattern/DoubleBeamPattern.java @@ -0,0 +1,137 @@ +//----------------------------------------------------------------------------// +// // +// D o u b l e B e a m P a t t e r n // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.glyph.pattern; + +import omr.glyph.*; +import omr.glyph.GlyphNetwork; +import omr.glyph.Glyphs; +import omr.glyph.Grades; +import omr.glyph.Shape; +import omr.glyph.facets.Glyph; + +import omr.sheet.SystemInfo; + +import omr.util.Predicate; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.awt.Rectangle; +import java.util.Arrays; +import java.util.Set; + +/** + * Class {@code DoubleBeamPattern} looks for BEAM_2 shape as compound + * for beams with just one stem. + * + * @author Hervé Bitteur + */ +public class DoubleBeamPattern + extends GlyphPattern +{ + //~ Static fields/initializers --------------------------------------------- + + /** Usual logger utility */ + private static final Logger logger = LoggerFactory.getLogger( + DoubleBeamPattern.class); + + //~ Constructors ----------------------------------------------------------- + //-------------------// + // DoubleBeamPattern // + //-------------------// + /** + * Creates a new DoubleBeamPattern object. + * + * @param system the system to process + */ + public DoubleBeamPattern (SystemInfo system) + { + super("DoubleBeam", system); + } + + //~ Methods ---------------------------------------------------------------- + //------------// + // runPattern // + //------------// + @Override + public int runPattern () + { + int nb = 0; + + for (final Glyph beam : system.getGlyphs()) { + if ((beam.getShape() != Shape.BEAM) + || beam.isManualShape() + || (beam.getStemNumber() != 1)) { + continue; + } + + if (beam.isVip() || logger.isDebugEnabled()) { + logger.info("Checking single-stem beam #{}", beam.getId()); + } + + final Glyph stem = beam.getFirstStem(); + + // Look for a beam glyph next to it + final Rectangle beamBox = beam.getBounds(); + beamBox.grow(1, 1); + + Set candidates = Glyphs.lookupGlyphs( + system.getGlyphs(), + new Predicate() + { + @Override + public boolean check (Glyph glyph) + { + return (glyph != stem) && (glyph != beam) + && (glyph.getShape() == Shape.BEAM) + && glyph.getBounds() + .intersects(beamBox); + } + }); + + for (Glyph candidate : candidates) { + if (beam.isVip() + || candidate.isVip() + || logger.isDebugEnabled()) { + logger.info("Beam candidate #{}", candidate); + } + + Glyph compound = system.buildTransientCompound( + Arrays.asList(beam, candidate)); + Evaluation eval = GlyphNetwork.getInstance() + .vote( + compound, + system, + Grades.noMinGrade); + + if (eval != null) { + // Assign and insert into system & lag environments + compound = system.addGlyph(compound); + compound.setEvaluation(eval); + + if (compound.isVip() || logger.isDebugEnabled()) { + logger.info( + "Compound #{} built as {}", + compound.getId(), + compound.getEvaluation()); + } + + nb++; + + break; + } + } + } + + return nb; + } +} diff --git a/src/main/omr/glyph/pattern/FermataDotPattern.java b/src/main/omr/glyph/pattern/FermataDotPattern.java new file mode 100644 index 0000000..42b6152 --- /dev/null +++ b/src/main/omr/glyph/pattern/FermataDotPattern.java @@ -0,0 +1,141 @@ +//----------------------------------------------------------------------------// +// // +// F e r m a t a D o t P a t t e r n // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.glyph.pattern; + +import omr.glyph.*; +import omr.glyph.Shape; +import omr.glyph.facets.Glyph; + +import omr.sheet.SystemInfo; + +import omr.util.Predicate; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.awt.Rectangle; +import java.util.Arrays; +import java.util.List; +import java.util.Set; + +/** + * Class {@code FermataDotPattern} looks for FERMATA & FERMATA_BELOW + * shaped-glyphs to make sure that the "associated" dot is not + * assigned anything else (such as staccato). + * + * @author Hervé Bitteur + */ +public class FermataDotPattern + extends GlyphPattern +{ + //~ Static fields/initializers --------------------------------------------- + + /** Usual logger utility */ + private static final Logger logger = LoggerFactory.getLogger( + FermataDotPattern.class); + + /** Dot avatars */ + private static final List dots = Arrays.asList( + Shape.DOT_set, + Shape.AUGMENTATION_DOT, + Shape.STACCATO); + + //~ Constructors ----------------------------------------------------------- + //-------------------// + // FermataDotPattern // + //-------------------// + /** + * Creates a new FermataDotPattern object. + * + * @param system the system to process + */ + public FermataDotPattern (SystemInfo system) + { + super("FermataDot", system); + } + + //~ Methods ---------------------------------------------------------------- + //------------// + // runPattern // + //------------// + @Override + public int runPattern () + { + int nb = 0; + + for (final Glyph fermata : system.getGlyphs()) { + if ((fermata.getShape() != Shape.FERMATA) + && (fermata.getShape() != Shape.FERMATA_BELOW)) { + continue; + } + + if (fermata.isVip() || logger.isDebugEnabled()) { + logger.info( + "Checking fermata #{} {}", + fermata.getId(), + fermata.getEvaluation()); + } + + // Find related dot within glyph box + final Rectangle box = fermata.getBounds(); + + Set candidates = Glyphs.lookupGlyphs( + system.getGlyphs(), + new Predicate() + { + @Override + public boolean check (Glyph glyph) + { + return (glyph != fermata) + && glyph.getBounds() + .intersects(box) + && dots.contains(glyph.getShape()); + } + }); + + for (Glyph candidate : candidates) { + if (fermata.isVip() + || candidate.isVip() + || logger.isDebugEnabled()) { + logger.info("Dot candidate #{}", candidate); + } + + Glyph compound = system.buildTransientCompound( + Arrays.asList(fermata, candidate)); + Evaluation eval = GlyphNetwork.getInstance() + .vote( + compound, + system, + Grades.noMinGrade); + + if (eval != null) { + // Assign and insert into system & nest environments + compound = system.addGlyph(compound); + compound.setEvaluation(eval); + + if (compound.isVip() || logger.isDebugEnabled()) { + logger.info( + "Compound #{} built as {}", + compound.getId(), + compound.getEvaluation()); + } + + nb++; + + break; + } + } + } + + return nb; + } +} diff --git a/src/main/omr/glyph/pattern/FlagPattern.java b/src/main/omr/glyph/pattern/FlagPattern.java new file mode 100644 index 0000000..724e11c --- /dev/null +++ b/src/main/omr/glyph/pattern/FlagPattern.java @@ -0,0 +1,144 @@ +//----------------------------------------------------------------------------// +// // +// F l a g P a t t e r n // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.glyph.pattern; + +import omr.constant.ConstantSet; + +import omr.glyph.Shape; +import omr.glyph.ShapeSet; +import omr.glyph.facets.Glyph; + +import omr.sheet.Scale; +import omr.sheet.SystemInfo; + +import omr.util.HorizontalSide; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.awt.Rectangle; + +/** + * Class {@code FlagPattern} removes flags for which the related stem + * has no attached head (or at least some significant no-shape stuff). + * + * @author Hervé Bitteur + */ +public class FlagPattern + extends GlyphPattern +{ + //~ Static fields/initializers --------------------------------------------- + + /** Specific application parameters */ + private static final Constants constants = new Constants(); + + /** Usual logger utility */ + private static final Logger logger = LoggerFactory.getLogger( + FlagPattern.class); + + //~ Constructors ----------------------------------------------------------- + //-------------// + // FlagPattern // + //-------------// + /** + * Creates a new FlagPattern object. + * + * @param system the system to process + */ + public FlagPattern (SystemInfo system) + { + super("Flag", system); + } + + //~ Methods ---------------------------------------------------------------- + //------------// + // runPattern // + //------------// + @Override + public int runPattern () + { + int nb = 0; + + for (Glyph flag : system.getGlyphs()) { + if (!ShapeSet.Flags.contains(flag.getShape()) + || flag.isManualShape()) { + continue; + } + + if (flag.isVip() || logger.isDebugEnabled()) { + logger.info("Checking flag #{}", flag.getId()); + } + + Glyph stem = flag.getStem(HorizontalSide.LEFT); + + if (stem == null) { + if (flag.isVip() || logger.isDebugEnabled()) { + logger.info("No left stem for flag #{}", flag.getId()); + } + + flag.setShape(null); + nb++; + + break; + } + + // Look for other stuff on the stem, whatever the side + Rectangle stemBox = system.stemBoxOf(stem); + boolean found = false; + + for (Glyph g : system.lookupIntersectedGlyphs(stemBox, stem, flag)) { + // We are looking for head (or some similar large stuff) + Shape shape = g.getShape(); + + if (ShapeSet.NoteHeads.contains(shape) + || ((shape == null) + && (g.getNormalizedWeight() >= constants.minStuffWeight.getValue()))) { + if (flag.isVip() || logger.isDebugEnabled()) { + logger.info("Confirmed flag #{}", flag.getId()); + } + + found = true; + + break; + } + } + + if (!found) { + // Deassign this flag w/ no head neighbor + if (flag.isVip() || logger.isDebugEnabled()) { + logger.info("Cancelled flag #{}", flag.getId()); + } + + flag.setShape(null); + nb++; + } + } + + return nb; + } + + //~ Inner Classes ---------------------------------------------------------- + //-----------// + // Constants // + //-----------// + private static final class Constants + extends ConstantSet + { + //~ Instance fields ---------------------------------------------------- + + // + Scale.AreaFraction minStuffWeight = new Scale.AreaFraction( + 0.5, + "Minimum weight for a stem stuff"); + + } +} diff --git a/src/main/omr/glyph/pattern/FortePattern.java b/src/main/omr/glyph/pattern/FortePattern.java new file mode 100644 index 0000000..8f2ad75 --- /dev/null +++ b/src/main/omr/glyph/pattern/FortePattern.java @@ -0,0 +1,140 @@ +//----------------------------------------------------------------------------// +// // +// F o r t e P a t t e r n // +// // +//----------------------------------------------------------------------------// +// // +// Copyright (C) Hervé Bitteur 2000-2010. All rights reserved. // +// This software is released under the GNU General Public License. // +// Goto http://kenai.com/projects/audiveris to report bugs or suggestions. // +//----------------------------------------------------------------------------// +// +package omr.glyph.pattern; + +import omr.glyph.CompoundBuilder; +import omr.glyph.Evaluation; +import omr.glyph.Grades; +import omr.glyph.Shape; +import omr.glyph.facets.Glyph; + +import omr.sheet.SystemInfo; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.awt.Rectangle; +import java.util.EnumSet; + +/** + * Class {@code FortePattern} uses easily recognized Forte signs ("f") to + * check the glyph next to them of the left, which is harder to recognize. + * It can only be "m" (mezzo), "r" (rinforzando) or "s" (sforzando). + * + * @author Hervé Bitteur + */ +public class FortePattern + extends GlyphPattern +{ + //~ Static fields/initializers --------------------------------------------- + + /** Usual logger utility */ + private static final Logger logger = LoggerFactory.getLogger( + FortePattern.class); + + /** Pre-forte shapes */ + public static final EnumSet forteNeighbors = EnumSet.of( + Shape.DYNAMICS_CHAR_M, + Shape.DYNAMICS_CHAR_R, + Shape.DYNAMICS_CHAR_S); + + //~ Constructors ----------------------------------------------------------- + //--------------// + // FortePattern // + //--------------// + /** + * Creates a new FortePattern object. + * + * @param system the system to process + */ + public FortePattern (SystemInfo system) + { + super("Forte", system); + } + + //~ Methods ---------------------------------------------------------------- + //------------// + // runPattern // + //------------// + @Override + public int runPattern () + { + int nb = 0; + + for (Glyph forte : system.getGlyphs()) { + // Focus on forte shaped glyphs + if (forte.getShape() == Shape.DYNAMICS_F) { + Glyph compound = system.buildCompound( + forte, + false, + system.getGlyphs(), + new ForteAdapter( + system, + Grades.forteMinGrade, + forteNeighbors)); + + if (compound != null) { + nb++; + } + } + } + + return nb; + } + + //~ Inner Classes ---------------------------------------------------------- + //---------------// + // ForteAdapter // + //---------------// + /** + * Adapter to actively search a Forte-compatible entity near the Forte glyph + */ + private final class ForteAdapter + extends CompoundBuilder.TopShapeAdapter + { + //~ Constructors ------------------------------------------------------- + + public ForteAdapter (SystemInfo system, + double minGrade, + EnumSet desiredShapes) + { + super(system, minGrade, desiredShapes); + } + + //~ Methods ------------------------------------------------------------ + @Override + public Rectangle computeReferenceBox () + { + Rectangle rect = seed.getBounds(); + Rectangle leftBox = new Rectangle( + rect.x, + rect.y + (rect.height / 3), + rect.width / 3, + rect.height / 3); + seed.addAttachment("fl", leftBox); + + return rect; + } + + @Override + public Evaluation getChosenEvaluation () + { + return new Evaluation(chosenEvaluation.shape, Evaluation.ALGORITHM); + } + + @Override + public boolean isCandidateSuitable (Glyph glyph) + { + return true; + } + } +} diff --git a/src/main/omr/glyph/pattern/GlyphPattern.java b/src/main/omr/glyph/pattern/GlyphPattern.java new file mode 100644 index 0000000..0139ffd --- /dev/null +++ b/src/main/omr/glyph/pattern/GlyphPattern.java @@ -0,0 +1,76 @@ +//----------------------------------------------------------------------------// +// // +// G l y p h P a t t e r n // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.glyph.pattern; + +import omr.sheet.Scale; +import omr.sheet.SystemInfo; + +/** + * Class {@code GlyphPattern} describes a specific pattern applied on + * glyphs of a given system. + * + * @author Hervé Bitteur + */ +public abstract class GlyphPattern +{ + //~ Instance fields -------------------------------------------------------- + + /** Name for debugging */ + public final String name; + + /** Related system */ + protected final SystemInfo system; + + /** System scale */ + protected final Scale scale; + + //~ Constructors ----------------------------------------------------------- + //--------------// + // GlyphPattern // + //--------------// + /** + * Creates a new GlyphPattern object. + * + * @param name the unique name for this pattern + * @param system the related system + */ + public GlyphPattern (String name, + SystemInfo system) + { + this.name = name; + this.system = system; + + scale = system.getSheet() + .getScale(); + } + + //~ Methods ---------------------------------------------------------------- + //------------// + // runPattern // + //------------// + /** + * This method runs the pattern and reports the number of modified + * glyphs. + * + * @return the number of modified glyphs + */ + public abstract int runPattern (); + + //----------// + // toString // + //----------// + @Override + public String toString () + { + return "S" + system.getId() + " pattern:" + name; + } +} diff --git a/src/main/omr/glyph/pattern/HiddenSlurPattern.java b/src/main/omr/glyph/pattern/HiddenSlurPattern.java new file mode 100644 index 0000000..2dedca3 --- /dev/null +++ b/src/main/omr/glyph/pattern/HiddenSlurPattern.java @@ -0,0 +1,104 @@ +//----------------------------------------------------------------------------// +// // +// H i d d e n S l u r P a t t e r n // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.glyph.pattern; + +import omr.constant.ConstantSet; + +import omr.glyph.facets.Glyph; + +import omr.sheet.Scale; +import omr.sheet.SystemInfo; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Class {@code HiddenSlurPattern} processes the significant glyphs + * which have not been assigned a shape, looking for a slur inside. + * + * @author Hervé Bitteur + */ +public class HiddenSlurPattern + extends GlyphPattern +{ + //~ Static fields/initializers --------------------------------------------- + + /** Specific application parameters */ + private static final Constants constants = new Constants(); + + /** Usual logger utility */ + private static final Logger logger = LoggerFactory.getLogger( + HiddenSlurPattern.class); + + //~ Constructors ----------------------------------------------------------- + //-------------------// + // HiddenSlurPattern // + //-------------------// + /** + * Creates a new HiddenSlurPattern object. + * + * @param system the containing system + */ + public HiddenSlurPattern (SystemInfo system) + { + super("HiddenSlur", system); + } + + //~ Methods ---------------------------------------------------------------- + //------------// + // runPattern // + //------------// + @Override + public int runPattern () + { + SlurInspector inspector = system.getSlurInspector(); + int successNb = 0; + final double minGlyphWeight = constants.minGlyphWeight.getValue(); + + for (Glyph glyph : system.getGlyphs()) { + if (glyph.isKnown() + || glyph.isManualShape() + || (glyph.getNormalizedWeight() < minGlyphWeight)) { + continue; + } + + if (glyph.isVip()) { + logger.info("Running HiddenSlur on {}", glyph.idString()); + } + + // Pickup a long thin section as seed + // Aggregate others progressively + Glyph newSlur = inspector.trimSlur(glyph); + + if ((newSlur != null) && (newSlur != glyph)) { + successNb++; + } + } + + return successNb; + } + + //~ Inner Classes ---------------------------------------------------------- + //-----------// + // Constants // + //-----------// + private static final class Constants + extends ConstantSet + { + //~ Instance fields ---------------------------------------------------- + + Scale.AreaFraction minGlyphWeight = new Scale.AreaFraction( + 0.5, + "Minimum normalized glyph weight to lookup a slur section"); + + } +} diff --git a/src/main/omr/glyph/pattern/LedgerPattern.java b/src/main/omr/glyph/pattern/LedgerPattern.java new file mode 100644 index 0000000..d59ce6a --- /dev/null +++ b/src/main/omr/glyph/pattern/LedgerPattern.java @@ -0,0 +1,273 @@ +//----------------------------------------------------------------------------// +// // +// L e d g e r P a t t e r n // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.glyph.pattern; + +import omr.constant.ConstantSet; + +import omr.glyph.CompoundBuilder; +import omr.glyph.Evaluation; +import omr.glyph.Grades; +import omr.glyph.Shape; +import omr.glyph.ShapeSet; +import omr.glyph.facets.Glyph; + +import omr.grid.StaffInfo; + +import omr.lag.Section; + +import omr.run.Orientation; + +import omr.sheet.HorizontalsBuilder; +import omr.sheet.Scale; +import omr.sheet.SystemInfo; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.awt.Rectangle; +import java.awt.geom.Point2D; +import java.util.ArrayList; +import java.util.EnumSet; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import java.util.SortedSet; + +/** + * Class {@code LedgerPattern} checks the related system for invalid ledgers. + * + * @author Hervé Bitteur + */ +public class LedgerPattern + extends GlyphPattern +{ + //~ Static fields/initializers --------------------------------------------- + + /** Specific application parameters */ + private static final Constants constants = new Constants(); + + /** Usual logger utility */ + private static final Logger logger = LoggerFactory.getLogger(LedgerPattern.class); + + /** Shapes acceptable for a ledger neighbor */ + public static final EnumSet ledgerNeighbors = EnumSet.noneOf( + Shape.class); + + static { + // ledgerNeighbors.add(Shape.GRACE_NOTE_SLASH); + // ledgerNeighbors.add(Shape.GRACE_NOTE_NO_SLASH); + ledgerNeighbors.addAll(ShapeSet.Notes.getShapes()); + ledgerNeighbors.addAll(ShapeSet.NoteHeads.getShapes()); + } + + //~ Instance fields -------------------------------------------------------- + /** Companion in charge of building ledgers */ + private final HorizontalsBuilder builder; + + /** Scale-dependent parameters */ + final int interChunkDx; + + final int interChunkDy; + + //~ Constructors ----------------------------------------------------------- + //---------------// + // LedgerPattern // + //---------------// + /** + * Creates a new LedgerPattern object. + * + * @param system the related system + */ + public LedgerPattern (SystemInfo system) + { + super("Ledger", system); + builder = system.getHorizontalsBuilder(); + interChunkDx = scale.toPixels(constants.interChunkDx); + interChunkDy = scale.toPixels(constants.interChunkDy); + } + + //~ Methods ---------------------------------------------------------------- + //------------// + // runPattern // + //------------// + @Override + public int runPattern () + { + int nb = 0; + + for (StaffInfo staff : system.getStaves()) { + Map> ledgerMap = staff.getLedgerMap(); + + for (Iterator>> iter = ledgerMap. + entrySet().iterator(); + iter.hasNext();) { + Entry> entry = iter.next(); + SortedSet ledgerSet = entry.getValue(); + List ledgerGlyphs = new ArrayList<>(); + + for (Glyph ledger : ledgerSet) { + ledgerGlyphs.add(ledger); + } + + // Process + for (Iterator it = ledgerSet.iterator(); it.hasNext();) { + Glyph ledger = it.next(); + Set neighbors = new HashSet<>(); + + if (isInvalid(ledger, neighbors)) { + // Check if we can forge a ledger-compatible neighbor + Glyph compound = system.buildCompound( + ledger, + false, + system.getGlyphs(), + new LedgerAdapter( + system, + Grades.ledgerNoteMinGrade, + ledgerNeighbors, + ledgerGlyphs)); + + if (compound == null) { + // Here, we have not found any convincing neighbor + // Let's invalid this pseudo ledger + logger.debug("Invalid ledger {}", ledger); + ledger.setShape(null); + it.remove(); + nb++; + } + } + } + + if (ledgerSet.isEmpty()) { + iter.remove(); + } + } + } + + return nb; + } + + //-----------// + // isInvalid // + //-----------// + private boolean isInvalid (Glyph ledgerGlyph, + Set neighborGlyphs) + { + // A short ledger must be stuck to either a note head or a stem + // (or a grace note) + List

allSections = new ArrayList<>(); + + for (Section section : ledgerGlyph.getMembers()) { + allSections.addAll(section.getSources()); + allSections.addAll(section.getTargets()); + allSections.addAll(section.getOppositeSections()); + } + + for (Section sct : allSections) { + Glyph g = sct.getGlyph(); + + if ((g != null) && (g != ledgerGlyph)) { + neighborGlyphs.add(g); + } + } + + for (Glyph glyph : neighborGlyphs) { + Shape shape = glyph.getShape(); + + if ((shape == Shape.STEM) || ledgerNeighbors.contains(shape)) { + return false; + } + } + + // If this a long ledger, check farther from the staff for a note with + // a ledger (full or chunk) + if (builder.isFullLedger(ledgerGlyph)) { + return false; + } + + return true; + } + + //~ Inner Classes ---------------------------------------------------------- + //-----------// + // Constants // + //-----------// + private static final class Constants + extends ConstantSet + { + //~ Instance fields ---------------------------------------------------- + + Scale.Fraction interChunkDx = new Scale.Fraction( + 1.5, + "Max horizontal distance between ledger chunks"); + + // + Scale.Fraction interChunkDy = new Scale.Fraction( + 0.2, + "Max vertical distance between ledger chunks"); + } + + //---------------// + // LedgerAdapter // + //---------------// + /** + * Adapter to actively search a ledger-compatible entity near the ledger + * chunk. + */ + private final class LedgerAdapter + extends CompoundBuilder.TopShapeAdapter + { + //~ Instance fields ---------------------------------------------------- + + private final List ledgerGlyphs; + + //~ Constructors ------------------------------------------------------- + public LedgerAdapter (SystemInfo system, + double minGrade, + EnumSet desiredShapes, + List ledgerGlyphs) + { + super(system, minGrade, desiredShapes); + this.ledgerGlyphs = ledgerGlyphs; + } + + //~ Methods ------------------------------------------------------------ + @Override + public Rectangle computeReferenceBox () + { + Point2D stop = seed.getStopPoint(Orientation.HORIZONTAL); + Rectangle rect = new Rectangle( + (int) Math.rint(stop.getX()), + (int) Math.rint(stop.getY()), + interChunkDx, + 0); + rect.grow(0, interChunkDy); + seed.addAttachment("-", rect); + + return rect; + } + + @Override + public Evaluation getChosenEvaluation () + { + return new Evaluation(chosenEvaluation.shape, Evaluation.ALGORITHM); + } + + @Override + public boolean isCandidateSuitable (Glyph glyph) + { + return !ledgerGlyphs.contains(glyph); + } + } +} diff --git a/src/main/omr/glyph/pattern/LeftOverPattern.java b/src/main/omr/glyph/pattern/LeftOverPattern.java new file mode 100644 index 0000000..0d65e51 --- /dev/null +++ b/src/main/omr/glyph/pattern/LeftOverPattern.java @@ -0,0 +1,114 @@ +//----------------------------------------------------------------------------// +// // +// L e f t O v e r P a t t e r n // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.glyph.pattern; + +import omr.constant.ConstantSet; + +import omr.glyph.Evaluation; +import omr.glyph.GlyphNetwork; +import omr.glyph.Grades; +import omr.glyph.ShapeEvaluator; +import omr.glyph.facets.Glyph; + +import omr.sheet.Scale; +import omr.sheet.SystemInfo; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Class {@code LeftOverPattern} processes the significant glyphs + * which have been left over. + * It addresses glyphs of non-assigned shape with significant weight, and + * assigns them the top 1 shape. + * + * @author Hervé Bitteur + */ +public class LeftOverPattern + extends GlyphPattern +{ + //~ Static fields/initializers --------------------------------------------- + + /** Specific application parameters */ + private static final Constants constants = new Constants(); + + /** Usual logger utility */ + private static final Logger logger = LoggerFactory.getLogger( + LeftOverPattern.class); + + //~ Constructors ----------------------------------------------------------- + //-----------------// + // LeftOverPattern // + //-----------------// + /** + * Creates a new LeftOverPattern object. + * + * @param system the containing system + */ + public LeftOverPattern (SystemInfo system) + { + super("LeftOver", system); + } + + //~ Methods ---------------------------------------------------------------- + //------------// + // runPattern // + //------------// + @Override + public int runPattern () + { + int successNb = 0; + final double minWeight = constants.minWeight.getValue(); + final ShapeEvaluator evaluator = GlyphNetwork.getInstance(); + + for (Glyph glyph : system.getGlyphs()) { + if (glyph.isKnown() + || glyph.isManualShape() + || (glyph.getNormalizedWeight() < minWeight)) { + continue; + } + + Evaluation vote = evaluator.vote( + glyph, + system, + Grades.leftOverMinGrade); + + if (vote != null) { + glyph = system.addGlyph(glyph); + glyph.setEvaluation(vote); + + if (logger.isDebugEnabled() || glyph.isVip()) { + logger.info("LeftOver {} vote: {}", glyph.idString(), vote); + } + + successNb++; + } + } + + return successNb; + } + + //~ Inner Classes ---------------------------------------------------------- + //-----------// + // Constants // + //-----------// + private static final class Constants + extends ConstantSet + { + //~ Instance fields ---------------------------------------------------- + + Scale.AreaFraction minWeight = new Scale.AreaFraction( + 0.3, + "Minimum normalized weight to be a left over glyph"); + + } +} diff --git a/src/main/omr/glyph/pattern/PatternsChecker.java b/src/main/omr/glyph/pattern/PatternsChecker.java new file mode 100644 index 0000000..ac7af1e --- /dev/null +++ b/src/main/omr/glyph/pattern/PatternsChecker.java @@ -0,0 +1,179 @@ +//----------------------------------------------------------------------------// +// // +// P a t t e r n s C h e c k e r // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.glyph.pattern; + +import omr.glyph.Grades; + +import omr.sheet.SystemInfo; + +import omr.text.TextPattern; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Class {@code PatternsChecker} gathers for a given system a series of + * specific patterns to process (verify, recognize, fix, ...) glyphs + * in their sheet environment. + * + * @author Hervé Bitteur + */ +public class PatternsChecker +{ + //~ Static fields/initializers --------------------------------------------- + + /** Usual logger utility */ + private static final Logger logger = LoggerFactory.getLogger( + PatternsChecker.class); + + //~ Instance fields -------------------------------------------------------- + // + /** Sequence of patterns to run. */ + private final GlyphPattern[] patterns; + + /** Dedicated system. */ + private final SystemInfo system; + + //~ Constructors ----------------------------------------------------------- + // + //-----------------// + // PatternsChecker // + //-----------------// + /** + * Creates a new PatternsChecker object. + * + * @param system the dedicated system + */ + public PatternsChecker (final SystemInfo system) + { + this.system = system; + + patterns = new GlyphPattern[]{ + // + new CaesuraPattern(system), new BeamHookPattern(system), + new DotPattern(system), + // Refresh ... + new RefreshPattern(system, false), new DoubleBeamPattern(system), + new FermataDotPattern(system), new FlagPattern(system), + new FortePattern(system), new HiddenSlurPattern(system), + new SplitPattern(system), new LedgerPattern(system), + new AlterPattern(system), new StemPattern(system), + system.getSlurInspector(), new BassPattern(system), + new ClefPattern(system), new TimePattern(system), + // Refresh ... + new RefreshPattern(system, true), + // + new TextPattern(system), + ///new TextCheckerPattern(system), // Debug stuff + + ///new ArticulationPattern(system), + + ///new SegmentationPattern(system), + new LeftOverPattern(system) + }; + } + + //~ Methods ---------------------------------------------------------------- + // + //-------------// + // runPatterns // + //-------------// + /** + * Run the sequence of pattern on the dedicated system + * + * @return the number of modifications made + */ + public boolean runPatterns () + { + int totalModifs = 0; + StringBuilder sb = new StringBuilder(); + + system.inspectGlyphs(Grades.symbolMinGrade, false); + + // final Step symbolsStep = Steps.valueOf(Steps.SYMBOLS); + // + // // Continuing, update UI + // SwingUtilities.invokeLater( + // new Runnable() + // { + // @Override + // public void run () + // { + // symbolsStep.displayUI(system.getSheet()); + // } + // }); + for (GlyphPattern pattern : patterns) { + logger.debug("Starting {}", pattern); + + system.removeInactiveGlyphs(); + + try { + int modifs = pattern.runPattern(); + + if (logger.isDebugEnabled()) { + sb.append(" ") + .append(pattern.name) + .append(":") + .append(modifs); + } + + totalModifs += modifs; + } catch (Throwable ex) { + logger.warn( + system.getLogPrefix() + " error running pattern " + + pattern.name, + ex); + } + } + + system.inspectGlyphs(Grades.symbolMinGrade, false); + + if (totalModifs > 0) { + logger.debug("S#{} Patterns{}", system.getId(), sb); + } + + return totalModifs != 0; + } + + //~ Inner Classes ---------------------------------------------------------- + // + //----------------// + // RefreshPattern // + //----------------// + /** + * Dummy pattern, just to refresh the system glyphs. + */ + private static class RefreshPattern + extends GlyphPattern + { + //~ Instance fields ---------------------------------------------------- + + private final boolean wide; + + //~ Constructors ------------------------------------------------------- + public RefreshPattern (SystemInfo system, + boolean wide) + { + super("Refresh", system); + this.wide = wide; + } + + //~ Methods ------------------------------------------------------------ + @Override + public int runPattern () + { + system.inspectGlyphs(Grades.symbolMinGrade, wide); + + return 0; + } + } +} diff --git a/src/main/omr/glyph/pattern/SlurInspector.java b/src/main/omr/glyph/pattern/SlurInspector.java new file mode 100644 index 0000000..b17241a --- /dev/null +++ b/src/main/omr/glyph/pattern/SlurInspector.java @@ -0,0 +1,1427 @@ +//----------------------------------------------------------------------------// +// // +// S l u r I n s p e c t o r // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.glyph.pattern; + +import omr.constant.Constant; +import omr.constant.ConstantSet; + +import omr.glyph.CompoundBuilder; +import omr.glyph.Evaluation; +import omr.glyph.Grades; +import omr.glyph.Shape; +import omr.glyph.ShapeSet; +import omr.glyph.facets.BasicGlyph; +import omr.glyph.facets.Glyph; + +import omr.grid.StaffInfo; + +import omr.lag.Section; +import omr.lag.Sections; + +import omr.math.Barycenter; +import omr.math.Circle; +import omr.math.PointsCollector; +import static omr.run.Orientation.*; + +import omr.sheet.Scale; +import omr.sheet.SystemInfo; + +import omr.util.HorizontalSide; +import static omr.util.HorizontalSide.*; +import omr.util.Wrapper; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.awt.Rectangle; +import java.awt.geom.CubicCurve2D; +import java.awt.geom.Line2D; +import java.awt.geom.Point2D; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.EnumSet; +import java.util.Iterator; +import java.util.List; + +/** + * Class {@code SlurInspector} encapsulates physical processing + * dedicated to inspection at system level of glyphs with SLUR shape. + * + * @author Hervé Bitteur + */ +public class SlurInspector + extends GlyphPattern +{ + //~ Static fields/initializers --------------------------------------------- + + /** + * TODO: + * - extendSlur() should extend glyph by glyph + * - extendSlurSection() & collectMemberSections() should be factorized + */ + /** Specific application parameters */ + private static final Constants constants = new Constants(); + + /** Usual logger utility */ + private static final Logger logger = LoggerFactory.getLogger(SlurInspector.class); + + /** Shapes suitable for extensions. */ + private static final EnumSet extShapes = EnumSet.copyOf( + ShapeSet.shapesOf( + ShapeSet.Dots, + ShapeSet.shapesOf(Shape.CLUTTER, Shape.CHARACTER, + Shape.LEDGER, Shape.TENUTO))); + + //~ Instance fields -------------------------------------------------------- + // + /** Compound adapter to extend slurs */ + private final SlurCompoundAdapter adapter; + + /** Scale-dependent parameters. */ + private final Parameters params; + + //~ Constructors ----------------------------------------------------------- + //---------------// + // SlurInspector // + //---------------// + /** + * Creates a new SlurInspector object. + * + * @param system The dedicated system + */ + public SlurInspector (SystemInfo system) + { + super("Slur", system); + + adapter = new SlurCompoundAdapter(system); + + params = new Parameters(system.getSheet().getScale()); + } + + //~ Methods ---------------------------------------------------------------- + //---------------// + // computeCircle // + //---------------// + /** + * Compute the circle which best approximates the pixels of a given + * collection of sections. + * We use a rather simple approach, based on 3 defining points (slur ending + * points, plus a middle point) which gives good results. + * If resulting distance is too high (and if slur width is large enough), + * we fall back using plain fitting on all sections points. + * + * @param sections the collection of sections to fit the circle upon + * @return the best circle possible + */ + public Circle computeCircle (Collection sections) + { + final Rectangle box = Sections.getBounds(sections); + + // Cumulate points from sections + PointsCollector collector = new PointsCollector(box); + + for (Section section : sections) { + section.cumulate(collector); + } + + int[] intXX = collector.getXValues(); + int[] intYY = collector.getYValues(); + double[] xx = new double[collector.getSize()]; + double[] yy = new double[collector.getSize()]; + + for (int i = 0; i < xx.length; i++) { + xx[i] = intXX[i]; + yy[i] = intYY[i]; + } + + // We force 3 defining points + Point2D left = getSlurPointNearX(box.x, sections, box); + Point2D right = getSlurPointNearX(box.x + box.width, sections, box); + Point2D middle = getSlurPointNearX( + box.x + (box.width / 2), + sections, + box); + + // Adjust middle abscissa according to slur orientation + double slope = (right.getY() - left.getY()) / (right.getX() + - left.getX()); + Point2D inter = new Point2D.Double( + middle.getX(), + left.getY() + ((middle.getX() - left.getX()) * slope)); + double dy = middle.getY() - inter.getY(); + double dx = -dy * (slope / (1 + slope * slope)); + middle = getSlurPointNearX( + box.x + (box.width / 2) + (int) Math.rint(dx), + sections, + box); + + Circle circle = new Circle(left, middle, right, xx, yy); + + // Switch to points fitting, if needed + if ((circle.getDistance() > params.maxCircleDistance)) { + logger.debug("Using total fit for slur {}", box); + circle = new Circle(xx, yy); + } + + return circle; + } + + //-----------// + // getCircle // + //-----------// + /** + * Report the circle which best approximates the pixels of a given + * glyph. + * + * @param glyph The glyph to fit the circle on + * @return The best circle possible + */ + public Circle getCircle (Glyph glyph) + { + Circle circle = glyph.getCircle(); + + if (circle == null) { + circle = computeCircle(glyph.getMembers()); + glyph.setCircle(circle); + } + + return circle; + } + + //------------// + // runPattern // + //------------// + /** + * Check all the slur glyphs in the given system, and try to + * correct the invalid ones if any. + * + * @return the number of invalid slurs that are fixed + * + *

Synopsis: + *

+     *      + extendSlur()      // attempt to get to a valid larger slur
+     *          + extendSlurSections()
+     *      + isValid()
+     *      + trimSlur()        // attempt to get to a valid smaller slur
+     *          + collectMemberSections()
+     *          + detectIsolatedSections()
+     *          + buildFinalSlur()
+     * 
+ */ + @Override + public int runPattern () + { + // Make a list of all slur glyphs to be checked in this system + // (So as to free the system glyphs list for on-the-fly modifications) + List slurs = new ArrayList<>(); + int modifs = 0; + + for (Glyph glyph : system.getGlyphs()) { + if (glyph.getShape() == Shape.SLUR) { + if (glyph.isManualShape()) { + glyph.addAttachment("^", getCircle(glyph).getCurve()); + } else { + slurs.add(glyph); + } + } + } + + // First pass to extend existing slurs + List toAdd = new ArrayList<>(); + + for (Iterator it = slurs.iterator(); it.hasNext();) { + Glyph slur = it.next(); + + // Skip slurs just been 'merged' with another one + if (!slur.isActive()) { + continue; + } + + // Extend this slur as much as possible + try { + Glyph largerSlur = extendSlur(slur); + + if (largerSlur != null) { + toAdd.add(largerSlur); + it.remove(); + modifs++; + } + } catch (NoSlurCurveException ex) { + if (logger.isDebugEnabled()) { + logger.info("{}Abnormal curve slur#{}", + system.getSheet().getLogPrefix(), slur.getId()); + } + + slur.setShape(null); + it.remove(); + modifs++; + } catch (Exception ex) { + logger.warn("Error in extending slur#" + slur.getId(), ex); + } + } + + slurs.addAll(toAdd); + + // Second pass to check each slur validity + for (Glyph slur : slurs) { + // Skip slurs just been 'merged' with another one + if (!slur.isActive()) { + continue; + } + + // Check slur validity + if (!isValid(slur)) { + // Extension has already been tried to no avail + // So, just try to trim the slur down + try { + if (trimSlur(slur) == null) { + slur.setShape(null); + } + } catch (Exception ex) { + logger.warn( + "Error in trimming slur#" + slur.getId(), + ex); + } + + modifs++; + } else if (logger.isDebugEnabled()) { + logger.debug("Valid slur {}", slur.getId()); + slur.addAttachment("^", getCircle(slur).getCurve()); + } + } + + return modifs; + } + + //----------// + // trimSlur // + //----------// + /** + * For large glyphs, we suspect a slur with a stuck object, + * so the strategy is to rebuild the true Slur portions from the + * underlying sections. + * + * @param oldSlur the spurious slur + * @return the extracted slur glyph, if any + */ + public Glyph trimSlur (Glyph oldSlur) + { + /** + * Sections are first ordered by decreasing weight and + * continuously tested via the distance to the best + * approximating circle. + * Sections whose weight is under a given threshold are appended to the + * slur only if the resulting circle distance gets lower. + * + * The "good" sections are put into the "kept" collection. + * Sections left over are put into the "left" collection in order to be + * used to rebuild the stuck object(s). + */ + if (oldSlur.isVip() || logger.isDebugEnabled()) { + logger.info("Trimming slur {}", oldSlur.idString()); + } + + // Get a COPY of the member list, sorted by decreasing weight */ + List
members = new ArrayList<>(oldSlur.getMembers()); + Collections.sort(members, Section.reverseWeightComparator); + + // Find the suitable seed + Wrapper seedDist = new Wrapper<>(); + Section seedSection = findSeedSection(members, seedDist); + + // If no significant section has been found, just give up + if (seedSection == null) { + if (oldSlur.getShape() == Shape.SLUR) { + oldSlur.setShape(null); + } + + return null; + } + + // Sections collected (including seedSection) + List
collected = collectMemberSections( + members, + seedSection, + seedDist.value); + + // Sections left over + List
left = new ArrayList<>(members); + left.removeAll(collected); + + // Sections too far from the other ones + List
isolated = detectIsolatedSections(seedSection, collected); + collected.removeAll(isolated); + left.addAll(isolated); + + if (!collected.isEmpty()) { + Glyph newSlur = null; + + try { + // Make sure we do have a suitable slur + newSlur = buildFinalSlur(collected); + + if (newSlur != null) { + if (oldSlur.isVip() || logger.isDebugEnabled()) { + logger.info("Trimmed slur #{} as smaller #{}", + oldSlur.getId(), newSlur.getId()); + } + } else { + if (oldSlur.isVip() || logger.isDebugEnabled()) { + logger.info("Giving up slur #{} w/ {}", + oldSlur.getId(), collected); + } + + left.addAll(collected); + } + + return newSlur; + } catch (Exception ex) { + left.addAll(collected); + + return null; + } finally { + // Remove former oldSlur glyph + if (oldSlur != newSlur) { + oldSlur.setShape(null); + + // Free the sections left over (useful???) + for (Section section : left) { + section.setGlyph(null); + } + } + } + } else { + logger.warn("{} No section left when trimming slur #{}", + system.getScoreSystem().getContextString(), oldSlur.getId()); + + return null; + } + } + + //----------------// + // buildFinalSlur // + //----------------// + /** + * Try to build a valid slur from a collection of sections. + * + * @param sections the slur sections + * @return the valid slur if any, null otherwise + */ + private Glyph buildFinalSlur (List
sections) + { + if (null == getInvalidity(sections, null)) { + // Build new slur glyph with sections kept + Glyph newGlyph = new BasicGlyph(params.interline); + + for (Section section : sections) { + newGlyph.addSection(section, Glyph.Linking.LINK_BACK); + } + + // Beware, the newGlyph may now belong to a different system + SystemInfo newSystem = system.getSheet().getSystemOf(newGlyph); + + // Check whether SLUR is not forbidden for this glyph + newGlyph = newSystem.registerGlyph(newGlyph); + + if (newGlyph.isShapeForbidden(Shape.SLUR)) { + return null; + } + + newGlyph = newSystem.addGlyph(newGlyph); + newGlyph.setShape(Shape.SLUR); + + newGlyph.addAttachment("^", getCircle(newGlyph).getCurve()); + + return newGlyph; + } else { + return null; + } + } + + //-----------------------// + // collectMemberSections // + //-----------------------// + /** + * From the provided members, find all sections well located + * on the slur circle, including the seed section. + * We start from the best seed section, then grow incrementally with + * compatible sections, continuously checking distance to resulting circle. + * + * @param members the glyph sections + * @param seedSection the starting seed section + * @param lastDistance the fitting distance to current circle + * @return the list of sections collected (including seed section) + */ + private List
collectMemberSections (List
members, + Section seedSection, + double lastDistance) + { + List
collected = new ArrayList<>(); + + // We impose the seed + collected.add(seedSection); + + for (Section section : members) { + section.setProcessed(false); + } + + // Let's grow the seed incrementally as much as possible + Rectangle slurBox = seedSection.getBounds(); + seedSection.setProcessed(true); + + boolean growing = true; + + while (growing) { + growing = false; + + for (Section section : members) { + if (section.isProcessed()) { + continue; + } + + // Need connection + Rectangle sctBox = section.getBounds(); + sctBox.grow(1, 1); + + if (!sctBox.intersects(slurBox)) { + continue; + } + + logger.debug("Trying {}", section); + + // Try a circle + List
config = new ArrayList<>(collected); + config.add(section); + + try { + Circle circle = computeCircle(config); + double distance = circle.getDistance(); + logger.debug("dist={}", distance); + + if (distance <= extendedDistance(lastDistance)) { + collected.add(section); + lastDistance = distance; + section.setProcessed(true); + slurBox.add(section.getBounds()); + growing = true; + logger.debug("Keep {}", section); + } else { + logger.debug("Discard {}", section); + } + } catch (Exception ex) { + logger.debug("{} w/ {}", ex.getMessage(), section); + } + } + } + + return collected; + } + + //------------------------// + // detectIsolatedSections // + //------------------------// + /** + * Detect any section which is too far from the other ones. + * + * @param seedSection the initial seed section + * @param collected the sections collected, including seed section + * @return the collection of isolated sections found + */ + private List
detectIsolatedSections (Section seedSection, + List
collected) + { + final List
isolated = new ArrayList<>(collected); + final Rectangle slurBox = seedSection.getBounds(); + boolean makingProgress; + + do { + makingProgress = false; + + for (Iterator
it = isolated.iterator(); it.hasNext();) { + Section section = it.next(); + Rectangle sectBox = section.getBounds(); + sectBox.grow(params.slurBoxDx, params.slurBoxDy); + + if (sectBox.intersects(slurBox)) { + slurBox.add(sectBox); + it.remove(); + makingProgress = true; + } + } + } while (makingProgress); + + return isolated; + } + + //------------// + // extendSlur // + //------------// + /** + * Try to build a compound glyph with compatible neighboring + * glyphs, and test the validity of the resulting slur. + * + * @param root the slur glyph to extend + * @return the extended slur glyph if any, or null. A non-null glyph + * is returned IFF we have found a slur which is both larger than + * the initial slur and valid. + */ + private Glyph extendSlur (Glyph root) + { + // The best compound obtained so far + Glyph bestSlur = null; + + // Loop on extensions, left then right sides + for (HorizontalSide side : HorizontalSide.values()) { + // Extend as far as possible on the desired side + adapter.setSide(side); + + SideLoop: + while (true) { + if (root.isVip() || logger.isDebugEnabled()) { + logger.info("Trying to {} extend slur #{}", + side, root.getId()); + } + + // Look at neighboring glyphs (TODO: should be incremental?) + Glyph compound = system.buildCompound( + root, + true, // include seed + system.getGlyphs(), + adapter); + + if (compound != null) { + if (root.isVip() || logger.isDebugEnabled()) { + logger.info("Slur #{} {} extended as #{}", + root.getId(), side, compound.getId()); + + if (root.isVip()) { + compound.setVip(); + } + } + + bestSlur = compound; + root = compound; + } else { + // Look at neighboring sections + Glyph sectSlur = extendSlurSections(root, side); + + if (sectSlur != null) { + if (root.isVip() || logger.isDebugEnabled()) { + logger.info("sectSlur: {}", sectSlur); + } + + bestSlur = sectSlur; + } + + break SideLoop; // We are through on this side + } + } + } + + return bestSlur; + } + + //--------------------// + // extendSlurSections // + //--------------------// + /** + * Try to extend the provided slur with neighboring sections on + * the provided side. + * Starting from the slur seed, we incrementally aggregate compatible + * sections, sorted according to their distance to slur ending point. + * The process is stopped at the first failed attempt. + * + * @param root the slur glyph to extend + * @return the extended slur glyph if any, or null. A non-null glyph + * is returned IFF we have found a slur which is both larger than the + * initial slur and valid. + */ + private Glyph extendSlurSections (Glyph root, + HorizontalSide side) + { + // The best compound obtained so far + Glyph bestSlur = null; + + List
sections = new ArrayList<>(); + sections.addAll(system.getHorizontalSections()); + sections.addAll(system.getVerticalSections()); + + for (Section section : sections) { + Glyph glyph = section.getGlyph(); + + // Discard manual sections + if ((glyph != null) && glyph.isManualShape()) { + section.setProcessed(true); + } else { + section.setProcessed(false); + } + } + + for (Section section : root.getMembers()) { + section.setProcessed(true); + } + + // Initial conditions + adapter.setSide(side); + + // Loop on extensions + boolean growing = true; + + while (growing) { + growing = false; + + if (root.isVip() || logger.isDebugEnabled()) { + logger.info("Trying to section-extend slur #{}", root.getId()); + } + + // Process that slur, looking at neighboring sections + if (adapter.setSeed(root) == null) { + logger.warn("Null reference box"); + } + + // Retrieve good neighbors among the suitable sections + List
neighbors = new ArrayList<>(); + + for (Section section : sections) { + if (section.isVip()) { + logger.debug("Section {}", section); + } + + if (!section.isProcessed()) { + if (adapter.isSectionClose(section) + && adapter.isSectionSuitable(section)) { + neighbors.add(section); + section.setProcessed(true); + } + } + } + + // Let's try neighbors incrementally + if (!neighbors.isEmpty()) { + // Sort neighbors according to their distance from slur ending + Collections.sort(neighbors, adapter.sectionComparator); + + // Sections effectively added + List
added = new ArrayList<>(); + + for (Section section : neighbors) { + added.add(section); + + // slur config = seed sections + added sections + List
config = new ArrayList<>(added); + config.addAll(root.getMembers()); + + boolean sectionOk = false; + double distance = computeCircle(config).getDistance(); + logger.debug("dist={}", distance); + + if (distance <= adapter.extendedDistance()) { + Glyph compound = system.buildTransientGlyph(config); + + if (adapter.isCompoundValid(compound)) { + // Assign and insert into system & nest environments + compound = system.addGlyph(compound); + compound.setEvaluation( + adapter.getChosenEvaluation()); + + if (root.isVip() || logger.isDebugEnabled()) { + logger.info( + "Slur #{} extended as #{} with {}", + root.getId(), compound.getId(), + Sections.toString(added)); + + if (root.isVip()) { + compound.setVip(); + } + } + + bestSlur = compound; + root = compound; + adapter.setSeed(root); + growing = true; + sectionOk = true; + } + } + + if (!sectionOk) { + if (root.isVip() || logger.isDebugEnabled()) { + logger.info("Slur #{} excluding section#{}", + root.getId(), section); + } + + break; + } + } + } else { + return bestSlur; + } + } + + return bestSlur; + } + + //-----------------// + // findSeedSection // + //-----------------// + /** + * Find the best seed, which is chosen as the section with best + * circle distance among the sections whose weight is significant. + * + * @param sortedMembers the candidate sections, by decreasing weight + * @param seedDist (output) the distance measured for chosen seed + * @return the suitable seed, perhaps null + */ + private Section findSeedSection (List
sortedMembers, + Wrapper seedDist) + { + Section seedSection = null; + seedDist.value = Double.MAX_VALUE; + + for (Section seed : sortedMembers) { + // Check minimum weight + int weight = seed.getWeight(); + + if (weight < params.minChunkWeight) { + break; // Since sections are sorted + } + + // Check meanthickness + double thickness = Math.min( + seed.getMeanThickness(VERTICAL), + seed.getMeanThickness(HORIZONTAL)); + + if (thickness > params.maxChunkThickness) { + continue; + } + + Circle circle = computeCircle(Arrays.asList(seed)); + double dist = circle.getDistance(); + + if ((dist <= params.maxCircleDistance) && (dist < seedDist.value)) { + seedDist.value = dist; + seedSection = seed; + } + } + + if (logger.isDebugEnabled()) { + if (seedSection == null) { + logger.debug("No suitable seed section found"); + } else { + logger.debug("Seed section is {} dist:{}", + seedSection, seedDist.value); + } + } + + return seedSection; + } + + //---------------// + // getInvalidity // + //---------------// + /** + * Check validity of a collection of sections as a slur. + * + * @param sections the provided sections + * @param resulting circle if already known + * @return null if OK, otherwise the cause of invalidity + */ + private Object getInvalidity (Collection
sections, + Circle circle) + { + if (circle == null) { + circle = computeCircle(sections); + } + + // Check distance to circle + double dist = circle.getDistance(); + + if (dist > params.maxCircleDistance) { + return "distance " + (float) dist + " vs " + params.maxCircleDistance; + } + + // Check curve is computable + if (circle.getCurve() == null) { + return "no curve"; + } + + // Check radius + double radius = circle.getRadius(); + + if (radius < params.minCircleRadius) { + return "small radius " + (float) radius + " vs " + params.minCircleRadius; + } + + if (radius > params.maxCircleRadius) { + return "large radius " + (float) radius + " vs " + params.maxCircleRadius; + } + + // // Check curve bounds are rather close to slur box + // Rectangle curveBox = circle.getCurve() + // .getBounds(); + // + // double heightRatio = (double) curveBox.height / contourBox.height; + // + // if (heightRatio > constants.maxHeightRatio.getValue()) { + // if (logger.isDebugEnabled()) { + // logger.info( + // "Too high ratio: " + (float) heightRatio + + // " for curve box " + curveBox); + // } + // + // return false; + // } + return null; + } + + //-------------------// + // getSlurPointNearX // + //-------------------// + /** + * Retrieve the best slur point near the provided abscissa. + * + * @param x the provided abscissa + * @param sections the slur sections + * @param box the slur bounding box + * @return the best approximating point + */ + private Point2D getSlurPointNearX (int x, + Collection sections, + Rectangle box) + { + Rectangle roi = new Rectangle(x, box.y, 0, box.height); + Barycenter bary; + + do { + bary = new Barycenter(); + roi.grow(1, 0); + + for (Section section : sections) { + section.cumulate(bary, roi); + } + } while (bary.getWeight() == 0); + + return new Point2D.Double(bary.getX(), bary.getY()); + } + + //---------// + // isValid // + //---------// + /** + * Check validity of a glyph as a slur. + * + * @param glyph the glyph to check + * @return true if valid + */ + private boolean isValid (Glyph slur) + { + // Make sure we are not trying to reassign a blacklisted shape + if (slur.isShapeForbidden(Shape.SLUR)) { + return false; + } + + Object cause = getInvalidity(slur.getMembers(), slur.getCircle()); + + if (slur.isVip()) { + if (cause != null) { + logger.info("Invalid slur #{} : {}", slur.getId(), cause); + } else { + logger.info("Valid slur #{}", slur.getId()); + } + } + + return cause == null; + } + + //------------------// + // extendedDistance // + //------------------// + /** + * Report the maximum extended circle distance, knowing the + * circle distance of the current slur. + * + * @param lastDistance current slur fitting distance + * @return extended maximum distance + */ + private double extendedDistance (double lastDistance) + { + double ratio = constants.distanceExtensionRatio.getValue(); + return lastDistance + ratio * (params.maxCircleDistance - lastDistance); + } + + //~ Inner Classes ---------------------------------------------------------- + // + //----------------------// + // NoSlurCurveException // + //----------------------// + /** + * Used to signal an abnormal "slur" glyph, for which the curve + * cannot be computed or is degenerated to a straight line. + */ + private static class NoSlurCurveException + extends RuntimeException + { + } + + //---------------------// + // SlurCompoundAdapter // + //---------------------// + /** + * CompoundAdapter meant to process the extension of a slur. + */ + private class SlurCompoundAdapter + extends CompoundBuilder.AbstractAdapter + { + //~ Instance fields ---------------------------------------------------- + + // Underlying slur curve + protected CubicCurve2D curve; + + // Current fitting distance + protected double distance; + + // Current extension side + protected HorizontalSide side; + + // Current slur ending point + protected Point2D endPt; + + /** To sort sections according to the distance to slur end */ + public Comparator
sectionComparator = new Comparator
() + { + @Override + public int compare (Section s1, + Section s2) + { + // We use distance from section to adapter end point + return Double.compare(toEndSq(s1), toEndSq(s2)); + } + }; + + //~ Constructors ------------------------------------------------------- + public SlurCompoundAdapter (SystemInfo system) + { + // Note: minGrade value (0d) is irrelevant, since compound validity + // will be checked against specific slur characteristics rather + // than evaluation grade. + super(system, 0d); + } + + //~ Methods ------------------------------------------------------------ + /** + * Compute the extension box on the provided side. + * + * @return the extension box + * @see #setSide + */ + @Override + public Rectangle computeReferenceBox () + { + Rectangle sBox = seed.getBounds(); // Seed box + boolean isShort = sBox.width <= params.minSlurWidth; + Point2D cp; // Related control point + + if (isShort) { + // For short glyphs, circle/curve are not reliable + // so we use approximating line instead. + endPt = (side == LEFT) ? seed.getStartPoint(HORIZONTAL) + : seed.getStopPoint(HORIZONTAL); + cp = (side == LEFT) ? seed.getStopPoint(HORIZONTAL) + : seed.getStartPoint(HORIZONTAL); + } else { + endPt = (side == LEFT) ? curve.getP1() : curve.getP2(); + cp = (side == LEFT) ? curve.getCtrlP1() : curve.getCtrlP2(); + } + + // Exact ending point (?) + Rectangle roi = (side == LEFT) + ? new Rectangle(sBox.x, sBox.y, 1, sBox.height) + : new Rectangle((sBox.x + sBox.width) - 1, sBox.y, 1, sBox.height); + Point2D ep = seed.getRectangleCentroid(roi); + if (ep != null) { + if (side == RIGHT) { + ep.setLocation(ep.getX() + 1, ep.getY()); + } + } else { + ep = endPt; // Better than nothing + } + + StaffInfo staff = system.getStaffAt(endPt); + if (staff == null) { + // Weird case, where the slur crosses system boundaries + Point2D otherEnd = (side == LEFT) ? curve.getP2() : curve.getP1(); + staff = system.getStaffAt(otherEnd); + } + + // Is the slur end touching a staff line? + final double pitch = staff.pitchPositionOf(endPt); + final int intPitch = (int) Math.rint(pitch); + double target; + if ((Math.abs(intPitch) <= 4) && ((intPitch % 2) == 0)) { + // TODO: beware of vertical + double slope = (ep.getY() - cp.getY()) / (ep.getX() - cp.getX()); + + if (Math.abs(slope) <= params.maxTangentSlope) { + // This end touches a staff line, with horizontal tangent + target = params.targetLineTangentHypot; + } else { + // This end touches a staff line not horizontally + target = params.targetLineHypot; + } + } else { + // No staff line is involved, use smaller margins + target = params.targetHypot; + } + + Point2D cp2pt = new Point2D.Double( + endPt.getX() - cp.getX(), + endPt.getY() - cp.getY()); + double hypot = Math.hypot(cp2pt.getX(), cp2pt.getY()); + double lambda = target / hypot; + Point2D ext = new Point2D.Double( + ep.getX() + (lambda * cp2pt.getX()), + ep.getY() + (lambda * cp2pt.getY())); + + Rectangle rect = new Rectangle( + (int) Math.rint(Math.min(ext.getX(), ep.getX())), + (int) Math.rint(Math.min(ext.getY(), ep.getY())), + (int) Math.rint(Math.abs(ext.getX() - ep.getX())), + (int) Math.rint(Math.abs(ext.getY() - ep.getY()))); + + // Ensure minimum box height + if (rect.height < params.minExtensionHeight) { + rect.grow( + 0, + 1 + + (int) Math.rint( + (params.minExtensionHeight - rect.height) / 2.0)); + } + + seed.addAttachment(((side == LEFT) ? "e^" : "^e"), rect); + + return rect; + } + + @Override + public boolean isCandidateSuitable (Glyph glyph) + { + if (!glyph.isActive()) { + return false; // Safer + } + + // Check mean thickness + double thickness = Math.min( + glyph.getMeanThickness(VERTICAL), + glyph.getMeanThickness(HORIZONTAL)); + + if (thickness > params.maxChunkThickness) { + return false; + } + + // Check minimum weight + if (glyph.getWeight() < params.minExtensionWeight) { + return false; + } + + // Check shape + if (!glyph.isKnown()) { + return true; + } + + Shape shape = glyph.getShape(); + + if ((shape == Shape.SLUR) && !glyph.isManualShape()) { + return true; + } + + return (!glyph.isManualShape() && extShapes.contains(shape)) + || (glyph.getGrade() <= Grades.compoundPartMaxGrade); + } + + @Override + public boolean isCompoundValid (Glyph compound) + { + if (isValid(compound)) { + // Is distance still OK? + double compoundDistance = getCircle(compound).getDistance(); + if (compoundDistance <= extendedDistance()) { + chosenEvaluation = new Evaluation( + Shape.SLUR, + Evaluation.ALGORITHM); + + return true; + } else { + logger.debug("{} Degrading distance {} vs {}", + seed, compoundDistance, extendedDistance()); + } + } + + return false; + } + + public boolean isSectionClose (Section section) + { + return box.intersects(section.getBounds()); + } + + public boolean isSectionSuitable (Section section) + { + Glyph glyph = section.getGlyph(); + + if ((glyph != null) && !glyph.isActive()) { + return false; // Safer + } + + // Check meanthickness + double thickness = Math.min( + section.getMeanThickness(VERTICAL), + section.getMeanThickness(HORIZONTAL)); + + if (thickness > params.maxChunkThickness) { + return false; + } + + if ((glyph == null) || !glyph.isKnown()) { + // Check section weight + if (section.getWeight() >= params.minExtensionWeight) { + return true; + } else { + return false; + } + } + + Shape shape = glyph.getShape(); + + if (ShapeSet.Barlines.contains(shape) || (shape == Shape.SLUR)) { + return false; + } + + if (glyph.isManualShape()) { + return false; + } + + // Check shape grade + if (glyph.getGrade() > Grades.compoundPartMaxGrade) { + return false; + } + + return true; + } + + @Override + public Rectangle setSeed (Glyph seed) + { + box = null; + + // Side-effect: compute underlying curve + Circle circle = getCircle(seed); + + if (circle.getRadius().isInfinite()) { + throw new NoSlurCurveException(); + } + + curve = circle.getCurve(); + + if (curve == null) { + throw new NoSlurCurveException(); + } else { + seed.addAttachment("^", curve); + } + + // Side-effect: compute current circle distance + distance = circle.getDistance(); + + return super.setSeed(seed); + } + + /** + * Remember the desired extension side. + * + * @param side the desired side + */ + public void setSide (HorizontalSide side) + { + this.side = side; + } + + /** + * Report the maximum extended circle distance. + * + * @return extended maximum distance + */ + private double extendedDistance () + { + return SlurInspector.this.extendedDistance(distance); + } + + /** + * Report the (square) distance from the slur ending point to + * the provided section, according to the current side. + * + * @param section the provided section + * @return the square distance + */ + private double toEndSq (Section section) + { + Rectangle b = section.getBounds(); + + if (side == LEFT) { + // Use box right vertical + return new Line2D.Double( + b.x + b.width, + b.y, + b.x + b.width, + b.y + b.height).ptSegDistSq(endPt); + } else { + // Use box left vertical + return new Line2D.Double(b.x, b.y, b.x, b.y + b.height). + ptSegDistSq(endPt); + } + } + } + + //------------// + // Parameters // + //------------// + private static class Parameters + { + //~ Instance fields ---------------------------------------------------- + + final int interline; + + final int minChunkWeight; + + final int minExtensionWeight; + + final double maxChunkThickness; + + final int slurBoxDx; + + final int slurBoxDy; + + final int targetHypot; + + final int targetLineHypot; + + final int targetLineTangentHypot; + + final int minSlurWidth; + + final int minExtensionHeight; + + final double maxCircleDistance; + + final double minCircleRadius; + + final double maxCircleRadius; + + final double maxTangentSlope; + + //~ Constructors ------------------------------------------------------- + public Parameters (Scale scale) + { + interline = scale.getInterline(); + minChunkWeight = scale.toPixels(constants.minChunkWeight); + minExtensionWeight = scale.toPixels(constants.minExtensionWeight); + maxChunkThickness = scale.toPixels(constants.maxChunkThickness); + slurBoxDx = scale.toPixels(constants.slurBoxDx); + slurBoxDy = scale.toPixels(constants.slurBoxDy); + targetHypot = scale.toPixels(constants.slurBoxHypot); + targetLineHypot = scale.toPixels(constants.slurLineBoxHypot); + targetLineTangentHypot = scale.toPixels( + constants.slurLineTangentBoxHypot); + minSlurWidth = scale.toPixels(constants.minSlurWidth); + minExtensionHeight = scale.toPixels(constants.minExtensionHeight); + maxCircleDistance = scale.toPixelsDouble(constants.maxCircleDistance); + minCircleRadius = scale.toPixels(constants.minCircleRadius); + maxCircleRadius = scale.toPixels(constants.maxCircleRadius); + maxTangentSlope = constants.maxTangentSlope.getValue(); + } + } + + //-----------// + // Constants // + //-----------// + private static final class Constants + extends ConstantSet + { + //~ Instance fields ---------------------------------------------------- + + Scale.Fraction maxCircleDistance = new Scale.Fraction( + 0.006, + "Maximum distance to approximating circle for a slur"); + + Scale.Fraction minCircleRadius = new Scale.Fraction( + 0.7, + "Minimum circle radius for a slur"); + + Scale.Fraction maxCircleRadius = new Scale.Fraction( + 100, + "Maximum circle radius for a slur"); + + Scale.AreaFraction minChunkWeight = new Scale.AreaFraction( + 0.3, + "Minimum weight of a chunk to be part of slur computation"); + + Scale.AreaFraction minExtensionWeight = new Scale.AreaFraction( + 0.01, + "Minimum weight of a glyph to be considered for slur extension"); + + Scale.Fraction slurBoxDx = new Scale.Fraction( + 0.7, + "Extension abscissa when looking for slur compound"); + + Scale.Fraction slurBoxDy = new Scale.Fraction( + 0.4, + "Extension ordinate when looking for slur compound"); + + Scale.Fraction slurBoxHypot = new Scale.Fraction( + 0.9, + "Extension length when looking for line-free slur compound"); + + Scale.Fraction slurLineBoxHypot = new Scale.Fraction( + 1.5, + "Extension length when looking for line-touching slur compound"); + + Scale.Fraction slurLineTangentBoxHypot = new Scale.Fraction( + 3.0, + "Extension length when looking for line-tangent slur compound"); + + Scale.Fraction minSlurWidth = new Scale.Fraction( + 2, + "Minimum width to use curve rather than line for extension"); + + Scale.LineFraction minExtensionHeight = new Scale.LineFraction( + 2, + "Minimum height for extension box, specified as LineFraction"); + + Scale.LineFraction maxChunkThickness = new Scale.LineFraction( + 2, + "Maximum mean thickness of a chunk to be part of slur computation"); + + Constant.Ratio maxHeightRatio = new Constant.Ratio( + 2.0, + "Maximum height ratio between curve height and glyph height"); + + Constant.Double maxTangentSlope = new Constant.Double( + "tangent", + 0.05, + "Maximum slope for staff line tangent"); + + Constant.Ratio distanceExtensionRatio = new Constant.Ratio( + 0.67, + "Acceptable distance extension to maximum limit"); + + } +} diff --git a/src/main/omr/glyph/pattern/SplitPattern.java b/src/main/omr/glyph/pattern/SplitPattern.java new file mode 100644 index 0000000..ec93cc5 --- /dev/null +++ b/src/main/omr/glyph/pattern/SplitPattern.java @@ -0,0 +1,400 @@ +//----------------------------------------------------------------------------// +// // +// S p l i t P a t t e r n // +// // +//----------------------------------------------------------------------------// +// // +// Copyright (C) Hervé Bitteur 2000-2010. All rights reserved. // +// This software is released under the GNU General Public License. // +// Goto http://kenai.com/projects/audiveris to report bugs or suggestions. // +//----------------------------------------------------------------------------// +// +package omr.glyph.pattern; + +import omr.constant.ConstantSet; + +import omr.glyph.Evaluation; +import omr.glyph.GlyphNetwork; +import omr.glyph.GlyphSignature; +import omr.glyph.Grades; +import omr.glyph.Shape; +import omr.glyph.facets.BasicGlyph; +import omr.glyph.facets.Glyph; +import omr.glyph.facets.GlyphComposition.Linking; + +import omr.lag.Section; + +import omr.sheet.Scale; +import omr.sheet.SystemInfo; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.*; +import java.util.Map.Entry; + +/** + * Class {@code SplitPattern} tries to split large unknown glyphs into + * two valid chunks. + * + * @author Hervé Bitteur + */ +public class SplitPattern + extends GlyphPattern +{ + //~ Static fields/initializers --------------------------------------------- + + /** Specific application parameters */ + private static final Constants constants = new Constants(); + + /** Usual logger utility */ + private static final Logger logger = LoggerFactory.getLogger(SplitPattern.class); + + /** Set of shapes not accepted for glyph chunks. TODO: expand the set */ + private static final EnumSet invalidShapes = EnumSet.of( + Shape.CLUTTER, + Shape.NOISE); + + //~ Instance fields -------------------------------------------------------- + // Scale-dependent parameters + private final double minGlyphWeight; + + private final double minChunkWeight; + + //~ Constructors ----------------------------------------------------------- + //--------------// + // SplitPattern // + //--------------// + /** + * Creates a new SplitPattern object. + * + * @param system the dedicated system + */ + public SplitPattern (SystemInfo system) + { + super("Split", system); + + minGlyphWeight = scale.toPixels(constants.minGlyphWeight); + minChunkWeight = scale.toPixels(constants.minChunkWeight); + } + + //~ Methods ---------------------------------------------------------------- + //------------// + // runPattern // + //------------// + /** + * In the related system, look for heavy unknown glyphs which might + * be composed of several glyphs, each with a valid shape. + * + * @return the number of glyphs actually split + */ + @Override + public int runPattern () + { + int nb = 0; + + for (Glyph glyph : system.getGlyphs()) { + if (!glyph.isActive() + || glyph.isKnown() + || (glyph.getStemNumber() == 0) + || (glyph.getWeight() < minGlyphWeight)) { + continue; + } + + if (splitGlyph(glyph)) { + nb++; + } + } + + return nb; + } + + //--------// + // expand // + //--------// + /** + * Try to expand the provided glyph with the provided section. + * + * @param glyph to glyph to expand + * @param section the section to cehck for expandion pf the glyph + * @param master the master glyph which contains the section candidates + */ + private void expand (Glyph glyph, + Section section, + Glyph master) + { + // Check whether this section is suitable to expand the glyph + if (section.isProcessed() || (section.getGlyph() != master)) { + return; + } + + section.setProcessed(true); + + glyph.addSection(section, Glyph.Linking.NO_LINK_BACK); + + // Check recursively all sections linked to this one... + + // Incoming ones + for (Section source : section.getSources()) { + expand(glyph, source, master); + } + + // Outgoing ones + for (Section target : section.getTargets()) { + expand(glyph, target, master); + } + + // Sections from other orientation + for (Section other : section.getOppositeSections()) { + expand(glyph, other, master); + } + } + + //------------// + // splitGlyph // + //------------// + /** + * Try to split the provided master glyph into two valid glyph + * chunks. + * + * @param master the master glyph to split + * @return true if successful + */ + private boolean splitGlyph (Glyph master) + { + if (master.isVip()) { + logger.info("Trying to split G#{}", master.getId()); + } + + List splits = new ArrayList<>(); + + // Retrieve all binary splits of this glyph + for (Section seed : master.getMembers()) { + for (Section s : master.getMembers()) { + s.setProcessed(false); + } + + seed.setProcessed(true); // To not use this one + + Split split = new Split(master, seed); + List
others = new ArrayList<>(); + others.addAll(seed.getSources()); + others.addAll(seed.getTargets()); + others.addAll(seed.getOppositeSections()); + + for (Section s : others) { + if ((s.getGlyph() == master) && !s.isProcessed()) { + Glyph g = new BasicGlyph(scale.getInterline()); + expand(g, s, master); + + split.sigs.put(g.getSignature(), g); + } + } + + // Check if we have exactly two significant chunks + // (neglecting very small others) + int count = split.sigs.size(); + + for (GlyphSignature sig : split.sigs.keySet()) { + if (sig.getWeight() < minChunkWeight) { + count--; + } + } + + if (count == 2) { + if (master.isVip()) { + logger.info("Split candidate: {}", split); + } + + splits.add(split); + } + } + + if (splits.isEmpty()) { + return false; + } + + // Pickup the more weightwise balanced split + Collections.sort(splits); + + Split bestSplit = splits.get(0); + + bestSplit.register(system); + + if (master.isVip() || logger.isDebugEnabled()) { + logger.info("Checking {}", bestSplit); + } + + // Check whether each of the chunks can be assigned a valid shape + for (Glyph chunk : bestSplit.sigs.values()) { + if (chunk.getWeight() < minChunkWeight) { + continue; + } + + system.computeGlyphFeatures(chunk); + + Evaluation vote = GlyphNetwork.getInstance().vote( + chunk, + system, + Grades.partMinGrade); + + if ((vote == null) || invalidShapes.contains(vote.shape)) { + if (master.isVip() || logger.isDebugEnabled()) { + logger.info("No valid shape for chunk {}", chunk); + } + } else { + if (master.isVip() || logger.isDebugEnabled()) { + logger.info("{} for chunk {}", vote, chunk); + } + + chunk.setEvaluation(vote); + } + } + + // Now actually perform the split! + if (master.isVip() || logger.isDebugEnabled()) { + logger.info("{}Performing {}", system.getLogPrefix(), bestSplit); + } + + for (Glyph glyph : bestSplit.sigs.values()) { + system.addGlyph(glyph); + } + + return true; + } + + //~ Inner Classes ---------------------------------------------------------- + //-----------// + // Constants // + //-----------// + private static final class Constants + extends ConstantSet + { + //~ Instance fields ---------------------------------------------------- + + Scale.AreaFraction minGlyphWeight = new Scale.AreaFraction( + 1.0, + "Minimum normalized glyph weight to look for split"); + + // + Scale.AreaFraction minChunkWeight = new Scale.AreaFraction( + 0.025, + "Minimum normalized weight of a chunk to be part of a split"); + + } + + //-------// + // Split // + //-------// + /** + * Records information about a potential split of a master glyph. + */ + private static class Split + implements Comparable + { + //~ Instance fields ---------------------------------------------------- + + // Master glyph + private final Glyph master; + + // The section used for the split + private final Section seed; + + // The resulting glyph chunks, kept sorted by (increasing) weight + private final SortedMap sigs = new TreeMap<>(); + + //~ Constructors ------------------------------------------------------- + /** + * Create a split information. + * + * @param master the master glyph to be split + * @param seed the section where split would be performed + */ + public Split (Glyph master, + Section seed) + { + this.master = master; + this.seed = seed; + } + + //~ Methods ------------------------------------------------------------ + @Override + public int compareTo (Split that) + { + // Bigger first! + return Integer.signum( + that.getLowerWeight() - this.getLowerWeight()); + } + + public void register (SystemInfo system) + { + // Include section seed into the second largest chunk + Entry lowEntry = getSecondLargest(); + Glyph smallerGlyph = lowEntry.getValue(); + sigs.remove(lowEntry.getKey()); + smallerGlyph.addSection(seed, Linking.NO_LINK_BACK); + sigs.put(smallerGlyph.getSignature(), smallerGlyph); + + // Register the chunks (copy needed to avoid concurrent modifs) + Set> entries = new HashSet<>( + sigs.entrySet()); + + for (Entry entry : entries) { + Glyph value = entry.getValue(); + Glyph glyph = system.registerGlyph(value); + + if (glyph != value) { + GlyphSignature key = entry.getKey(); + sigs.remove(key); + sigs.put(key, glyph); + } + } + } + + @Override + public String toString () + { + StringBuilder sb = new StringBuilder("{SplitOf#"); + sb.append(master.getId()); + sb.append(" @S").append(seed.isVertical() ? "V" : "H").append(seed. + getId()); + + for (Entry entry : sigs.entrySet()) { + Glyph glyph = entry.getValue(); + sb.append(" #").append(glyph.getId()); + + Evaluation eval = glyph.getEvaluation(); + + if (eval != null) { + sb.append(":").append(eval); + } + } + + sb.append("}"); + + return sb.toString(); + } + + private int getLowerWeight () + { + return getSecondLargest().getKey().getWeight(); + } + + private Entry getSecondLargest () + { + // The map entries are sorted from small to large key + Entry prevEntry = null; + GlyphSignature lastKey = sigs.lastKey(); + + for (Entry entry : sigs.entrySet()) { + if (entry.getKey() == lastKey) { + return prevEntry; + } else { + prevEntry = entry; + } + } + + return prevEntry; + } + } +} diff --git a/src/main/omr/glyph/pattern/StemPattern.java b/src/main/omr/glyph/pattern/StemPattern.java new file mode 100644 index 0000000..d07efa1 --- /dev/null +++ b/src/main/omr/glyph/pattern/StemPattern.java @@ -0,0 +1,202 @@ +//----------------------------------------------------------------------------// +// // +// S t e m P a t t e r n // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.glyph.pattern; + +import omr.glyph.Evaluation; +import omr.glyph.ShapeEvaluator; +import omr.glyph.GlyphNetwork; +import omr.glyph.Grades; +import omr.glyph.Shape; +import omr.glyph.ShapeSet; +import omr.glyph.facets.Glyph; + +import omr.lag.Section; + +import omr.sheet.SystemInfo; + +import omr.util.Predicate; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * Class {@code StemPattern} is a GlyphInspector dedicated to the + * inspection of Stems at System level + * + * @author Hervé Bitteur + */ +public class StemPattern + extends GlyphPattern +{ + //~ Static fields/initializers --------------------------------------------- + + /** Usual logger utility */ + private static final Logger logger = LoggerFactory.getLogger(StemPattern.class); + + /** Predicate to filter reliable symbols attached to a stem. */ + public static final Predicate reliableStemSymbols = new Predicate() + { + @Override + public boolean check (Glyph glyph) + { + Shape shape = glyph.getShape(); + + boolean res = glyph.isWellKnown() + && ShapeSet.StemSymbols.contains(shape) + && (shape != Shape.BEAM_HOOK); + + return res; + } + }; + + //~ Constructors ----------------------------------------------------------- + /** + * Creates a new StemPattern object. + * + * @param system the dedicated system + */ + public StemPattern (SystemInfo system) + { + super("Stem", system); + } + + //~ Methods ---------------------------------------------------------------- + //------------// + // runPattern // + //------------// + /** + * In a specified system, look for all stems that should not be kept, + * rebuild surrounding glyphs and try to recognize them. + * If this action does not lead to some recognized symbol, then we restore + * the stems. + * + * @return the number of symbols recognized + */ + @Override + public int runPattern () + { + int nb = 0; + + Map> badsMap = new HashMap<>(); + + // Collect all undue stems + List SuspectedStems = new ArrayList<>(); + + for (Glyph glyph : system.getGlyphs()) { + if (glyph.isStem() && !glyph.isManualShape() && glyph.isActive()) { + Set goods = new HashSet<>(); + Set bads = new HashSet<>(); + glyph.getSymbolsBefore(reliableStemSymbols, goods, bads); + glyph.getSymbolsAfter(reliableStemSymbols, goods, bads); + + if (goods.isEmpty()) { + logger.debug("Suspected Stem {}", glyph); + + SuspectedStems.add(glyph); + + // Discard "bad" ones + badsMap.put(glyph, bads); + + for (Glyph g : bads) { + logger.debug("Deassigning bad glyph {}", g); + g.setShape((Shape) null); + } + } + } + } + + // Remove these stem glyphs since nearby stems are used for recognition + for (Glyph glyph : SuspectedStems) { + system.removeGlyph(glyph); + } + + // Extract brand new glyphs (removeInactiveGlyphs + retrieveGlyphs) + system.extractNewGlyphs(); + + // Try to recognize each glyph in turn + List symbols = new ArrayList<>(); + final ShapeEvaluator evaluator = GlyphNetwork.getInstance(); + + for (Glyph glyph : system.getGlyphs()) { + if (glyph.getShape() == null) { + Evaluation vote = evaluator.vote( + glyph, + system, + Grades.patternsMinGrade); + + if (vote != null) { + glyph.setEvaluation(vote); + + if (glyph.isWellKnown()) { + logger.debug("New symbol {}", glyph); + symbols.add(glyph); + nb++; + } + } + } + } + + // Keep stems that have not been replaced by symbols, definitively + // remove the others + for (Glyph stem : SuspectedStems) { + // Check if one of its section is now part of a symbol + boolean known = false; + Glyph glyph = null; + + for (Section section : stem.getMembers()) { + glyph = section.getGlyph(); + + if ((glyph != null) && glyph.isWellKnown()) { + known = true; + + break; + } + } + + if (!known) { + // Remove the newly created glyph + if (glyph != null) { + system.removeGlyph(glyph); + } + + // Restore the stem + system.addGlyph(stem); + + // Deassign the nearby glyphs that cannot accept a stem + Set bads = badsMap.get(stem); + + if (bads != null) { + for (Glyph g : bads) { + Shape shape = g.getShape(); + + if ((shape != null) + && !g.isManualShape() + && !ShapeSet.StemSymbols.contains(shape)) { + g.setShape(null); + } + } + } + } else if (stem.isVip()) { + logger.info("StemPattern deassigned stem#{}", stem.getId()); + } + } + + return nb; + } +} diff --git a/src/main/omr/glyph/pattern/TimePattern.java b/src/main/omr/glyph/pattern/TimePattern.java new file mode 100644 index 0000000..ac4e65d --- /dev/null +++ b/src/main/omr/glyph/pattern/TimePattern.java @@ -0,0 +1,248 @@ +//----------------------------------------------------------------------------// +// // +// T i m e P a t t e r n // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.glyph.pattern; + +import omr.constant.ConstantSet; + +import omr.glyph.CompoundBuilder; +import omr.glyph.Evaluation; +import omr.glyph.Grades; +import omr.glyph.Shape; +import omr.glyph.ShapeSet; +import omr.glyph.facets.Glyph; + +import omr.grid.StaffInfo; + +import omr.sheet.Scale; +import omr.sheet.SystemInfo; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.awt.Point; +import java.awt.Rectangle; +import java.util.EnumSet; + +/** + * Class {@code TimePattern} verifies the time signature glyphs. + * + * @author Hervé Bitteur + */ +public class TimePattern + extends GlyphPattern +{ + //~ Static fields/initializers --------------------------------------------- + + /** Specific application parameters */ + private static final Constants constants = new Constants(); + + /** Usual logger utility */ + private static final Logger logger = LoggerFactory.getLogger( + TimePattern.class); + + //~ Instance fields -------------------------------------------------------- + /** Specific compound adapter. */ + private final TimeSigAdapter adapter; + + /** Scale-dependent parameters. */ + private final Parameters params; + + //~ Constructors ----------------------------------------------------------- + /** + * Creates a new TimePattern object. + * + * @param system the containing system + */ + public TimePattern (SystemInfo system) + { + super("Time", system); + + params = new Parameters(system.getSheet().getScale()); + + adapter = new TimeSigAdapter( + system, + Grades.timeMinGrade, + ShapeSet.FullTimes); + } + + //~ Methods ---------------------------------------------------------------- + //------------// + // runPattern // + //------------// + /** + * Check that each staff begins with a time + * + * @return the number of times rebuilt + */ + @Override + public int runPattern () + { + int successNb = 0; + + for (Glyph glyph : system.getGlyphs()) { + if (!ShapeSet.Times.contains(glyph.getShape()) + || glyph.isManualShape()) { + continue; + } + + // We must find a time out of these glyphs + Glyph compound = system.buildCompound( + glyph, + true, + system.getGlyphs(), + adapter); + + if (compound != null) { + successNb++; + } + } + + return successNb; + } + + //~ Inner Classes ---------------------------------------------------------- + //-----------// + // Constants // + //-----------// + private static final class Constants + extends ConstantSet + { + //~ Instance fields ---------------------------------------------------- + + Scale.Fraction xOffset = new Scale.Fraction( + 0.25d, + "Core Time horizontal offset"); + + Scale.Fraction yOffset = new Scale.Fraction( + 0.25d, + "Core Time vertical offset"); + + Scale.Fraction timeWidth = new Scale.Fraction( + 1.6, + "Typical width of a time signature"); + + Scale.Fraction maxTimeWidth = new Scale.Fraction( + 4, + "Maximum width of a time signature"); + + Scale.Fraction maxTimeHeight = new Scale.Fraction( + 8, + "Maximum height of a time signature"); + + } + + //------------// + // Parameters // + //------------// + private static class Parameters + { + //~ Instance fields ---------------------------------------------------- + + final int xOffset; + + final int yOffset; + + final int timeWidth; + + final int maxTimeWidth; + + final int maxTimeHeight; + + //~ Constructors ------------------------------------------------------- + public Parameters (Scale scale) + { + xOffset = scale.toPixels(constants.xOffset); + yOffset = scale.toPixels(constants.yOffset); + timeWidth = scale.toPixels(constants.timeWidth); + maxTimeWidth = scale.toPixels(constants.maxTimeWidth); + maxTimeHeight = scale.toPixels(constants.maxTimeHeight); + } + } + + //----------------// + // TimeSigAdapter // + //----------------// + /** + * Compound adapter to search for a time sig shape + */ + private class TimeSigAdapter + extends CompoundBuilder.TopRawAdapter + { + //~ Constructors ------------------------------------------------------- + + public TimeSigAdapter (SystemInfo system, + double minGrade, + EnumSet desiredShapes) + { + super(system, minGrade, desiredShapes); + } + + //~ Methods ------------------------------------------------------------ + @Override + public Rectangle computeReferenceBox () + { + // Define the core box to intersect time glyph(s) + Point center = seed.getAreaCenter(); + StaffInfo staff = system.getStaffAt(center); + + Rectangle rect = seed.getBounds(); + + if (rect.width < params.timeWidth) { + rect.grow(params.timeWidth - rect.width, 0); + } + + rect.grow(-params.xOffset, 0); + rect.y = staff.getFirstLine() + .yAt(center.x) + params.yOffset; + rect.height = staff.getLastLine() + .yAt(center.x) - params.yOffset - rect.y; + + // Draw the time core box, for visual debug + seed.addAttachment("t", rect); + + return rect; + } + + @Override + public Evaluation getChosenEvaluation () + { + return new Evaluation(chosenEvaluation.shape, Evaluation.ALGORITHM); + } + + @Override + public boolean isCandidateClose (Glyph glyph) + { + if (super.isCandidateClose(glyph)) { + // Check dimension of resulting bounds + Rectangle result = glyph.getBounds() + .union(box); + + if ((result.width > params.maxTimeWidth) + || (result.height > params.maxTimeHeight)) { + logger.debug("Excluding too large {}", glyph); + + return false; + } else { + return true; + } + } else { + return false; + } + } + + @Override + public boolean isCandidateSuitable (Glyph glyph) + { + return !glyph.isManualShape() && glyph.isActive(); + } + } +} diff --git a/src/main/omr/glyph/pattern/package.html b/src/main/omr/glyph/pattern/package.html new file mode 100644 index 0000000..b64d353 --- /dev/null +++ b/src/main/omr/glyph/pattern/package.html @@ -0,0 +1,18 @@ + + + + + + Package omr.score.common + + + + +

+ Package for handling of patterns at glyph level, at a moment when + the surrounding glyphs have been recognized. +

+ + diff --git a/src/main/omr/glyph/ui/AttachmentHolder.java b/src/main/omr/glyph/ui/AttachmentHolder.java new file mode 100644 index 0000000..f03aa46 --- /dev/null +++ b/src/main/omr/glyph/ui/AttachmentHolder.java @@ -0,0 +1,61 @@ +//----------------------------------------------------------------------------// +// // +// A t t a c h m e n t H o l d e r // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.glyph.ui; + +import java.awt.Graphics2D; +import java.awt.Shape; +import java.util.Map; + +/** + * Interface {@code AttachmentHolder} defines the handling of visual + * attachments than can be displayed on user views. + * + * @author Hervé Bitteur + */ +public interface AttachmentHolder +{ + //~ Methods ---------------------------------------------------------------- + + /** + * Register an attachment with a key and a shape. + * This is meant to add arbitrary awt shapes to an entity, mainly for + * display and analysis purposes. + * + * @param id the attachment ID + * @param attachment Shape to attach. If null, attachment is ignored. + */ + void addAttachment (String id, + Shape attachment); + + /** + * Report a view on the map of attachments. + * + * @return a (perhaps empty) map of attachments + */ + Map getAttachments (); + + /** + * Remove all the attachments whose id begins with the provided + * prefix. + * + * @param prefix the beginning of ids + * @return the number of attachments removed + */ + int removeAttachments (String prefix); + + /** + * Render the attachments on the provided graphics context. + * + * @param g the graphics context + */ + void renderAttachments (Graphics2D g); +} diff --git a/src/main/omr/glyph/ui/BasicAttachmentHolder.java b/src/main/omr/glyph/ui/BasicAttachmentHolder.java new file mode 100644 index 0000000..71db9df --- /dev/null +++ b/src/main/omr/glyph/ui/BasicAttachmentHolder.java @@ -0,0 +1,118 @@ +//----------------------------------------------------------------------------// +// // +// B a s i c A t t a c h m e n t H o l d e r // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.glyph.ui; + +import omr.ui.Colors; + +import java.awt.Color; +import java.awt.Font; +import java.awt.Graphics2D; +import java.awt.Rectangle; +import java.awt.Shape; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Class {@code BasicAttachmentHolder} is a simple implementation of + * {@link AttachmentHolder} interface. + * + * @author Hervé Bitteur + */ +public class BasicAttachmentHolder + implements AttachmentHolder +{ + //~ Instance fields -------------------------------------------------------- + + /** Map for attachments */ + protected Map attachments = new HashMap<>(); + + //~ Methods ---------------------------------------------------------------- + //---------------// + // addAttachment // + //---------------// + @Override + public void addAttachment (String id, + Shape attachment) + { + if (attachment != null) { + attachments.put(id, attachment); + } + } + + //----------------// + // getAttachments // + //----------------// + @Override + public Map getAttachments () + { + return Collections.unmodifiableMap(attachments); + } + + //-------------------// + // removeAttachments // + //-------------------// + @Override + public int removeAttachments (String prefix) + { + // To avoid concurrent modifications + List toRemove = new ArrayList<>(); + + for (String key : attachments.keySet()) { + if (key.startsWith(prefix)) { + toRemove.add(key); + } + } + + for (String key : toRemove) { + attachments.remove(key); + } + + return toRemove.size(); + } + + //-------------------// + // renderAttachments // + //-------------------// + @Override + public void renderAttachments (Graphics2D g) + { + if (attachments.isEmpty() + || !ViewParameters.getInstance() + .isAttachmentPainting()) { + return; + } + + Color oldColor = g.getColor(); + g.setColor(Colors.ATTACHMENT); + + Font oldFont = g.getFont(); + g.setFont(oldFont.deriveFont(4f)); + + for (Map.Entry entry : attachments.entrySet()) { + Shape shape = entry.getValue(); + g.draw(shape); + + String key = entry.getKey(); + Rectangle rect = shape.getBounds(); + g.drawString( + key, + rect.x + (rect.width / 2), + rect.y + (rect.height / 2)); + } + + g.setFont(oldFont); + g.setColor(oldColor); + } +} diff --git a/src/main/omr/glyph/ui/EvaluationBoard.java b/src/main/omr/glyph/ui/EvaluationBoard.java new file mode 100644 index 0000000..3627171 --- /dev/null +++ b/src/main/omr/glyph/ui/EvaluationBoard.java @@ -0,0 +1,488 @@ +//----------------------------------------------------------------------------// +// // +// E v a l u a t i o n B o a r d // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.glyph.ui; + +import omr.constant.Constant; +import omr.constant.ConstantSet; + +import omr.glyph.Evaluation; +import omr.glyph.GlyphNetwork; +import omr.glyph.Glyphs; +import omr.glyph.Shape; +import omr.glyph.ShapeEvaluator; +import omr.glyph.facets.Glyph; + +import omr.selection.GlyphEvent; +import omr.selection.MouseMovement; +import omr.selection.SelectionHint; +import omr.selection.UserEvent; + +import omr.sheet.Sheet; +import omr.sheet.SystemInfo; + +import omr.ui.Board; +import omr.ui.Colors; +import omr.ui.util.Panel; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.jgoodies.forms.builder.PanelBuilder; +import com.jgoodies.forms.layout.CellConstraints; +import com.jgoodies.forms.layout.FormLayout; +import com.jgoodies.forms.layout.FormSpecs; + +import java.awt.Color; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.util.ArrayList; +import java.util.EnumSet; +import java.util.List; + +import javax.swing.JButton; +import javax.swing.JComponent; +import javax.swing.JLabel; +import javax.swing.JTextField; +import javax.swing.SwingConstants; + +/** + * Class {@code EvaluationBoard} is a board dedicated to the display of + * evaluation results performed by an evaluator. + * It also provides through buttons the ability to manually assign a shape to + * the glyph at hand. + * + * @author Hervé Bitteur + * @author Brenton Partridge + */ +class EvaluationBoard + extends Board +{ + //~ Static fields/initializers --------------------------------------------- + + /** Specific application parameters */ + private static final Constants constants = new Constants(); + + /** Usual logger utility */ + private static final Logger logger = LoggerFactory.getLogger( + EvaluationBoard.class); + + /** Events this board is interested in */ + private static final Class[] eventsRead = new Class[]{GlyphEvent.class}; + + /** Color for well recognized glyphs */ + private static final Color EVAL_GOOD_COLOR = new Color(100, 200, 100); + + /** Color for hardly recognized glyphs */ + private static final Color EVAL_SOSO_COLOR = new Color(150, 150, 150); + + //~ Instance fields -------------------------------------------------------- + /** The evaluator this display is related to */ + private final ShapeEvaluator evaluator = GlyphNetwork.getInstance(); + + /** Related glyphs controller */ + private final GlyphsController glyphsController; + + /** Related sheet */ + private final Sheet sheet; + + /** Pane for detailed info display about the glyph evaluation */ + private final Selector selector; + + /** Do we use GlyphChecker annotations? */ + private boolean useAnnotations; + + //~ Constructors ----------------------------------------------------------- + //-----------------// + // EvaluationBoard // + //-----------------// + /** + * Create a simplified passive evaluation board with one neural + * network evaluator. + * + * @param glyphModel the related glyph model + */ + public EvaluationBoard (GlyphsController glyphModel, + boolean expanded) + { + this(null, glyphModel, expanded); + useAnnotations = false; + } + + //-----------------// + // EvaluationBoard // + //-----------------// + /** + * Create an evaluation board with one neural network evaluator + * and the ability to force glyph shape. + * + * @param glyphController the related glyph controller + * @param sheet the related sheet, or null + */ + public EvaluationBoard (Sheet sheet, + GlyphsController glyphController, + boolean expanded) + { + super( + Board.EVAL, + glyphController.getNest().getGlyphService(), + eventsRead, + false, + expanded); + + this.glyphsController = glyphController; + this.sheet = sheet; + + selector = new Selector(); + defineLayout(); + useAnnotations = true; + } + + //~ Methods ---------------------------------------------------------------- + //----------// + // evaluate // + //----------// + /** + * Evaluate the glyph at hand, and display the result in the + * evaluator dedicated area. + * + * @param glyph the glyph at hand + */ + public void evaluate (Glyph glyph) + { + if ((glyph == null) + || (glyph.getShape() == Shape.STEM) + || glyph.isBar()) { + // Blank the output + selector.setEvals(null, null); + } else { + if (evaluator != null) { + if (useAnnotations) { + SystemInfo system = sheet.getSystemOf(glyph); + + if (system != null) { + selector.setEvals( + evaluator.evaluate( + glyph, + system, + selector.evalCount(), + constants.minGrade.getValue(), + EnumSet.of(ShapeEvaluator.Condition.CHECKED), + null), + glyph); + } + } else { + selector.setEvals( + evaluator.evaluate( + glyph, + null, + selector.evalCount(), + constants.minGrade.getValue(), + ShapeEvaluator.NO_CONDITIONS, + null), + glyph); + } + } + } + } + + //---------// + // onEvent // + //---------// + /** + * Call-back triggered when Glyph Selection has been modified. + * + * @param event the (Glyph) Selection + */ + @Override + public void onEvent (UserEvent event) + { + try { + // Ignore RELEASING + if (event.movement == MouseMovement.RELEASING) { + return; + } + + // Don't evaluate Added glyph, since this would hide Compound evaluation + if (event.hint == SelectionHint.LOCATION_ADD) { + return; + } + + if (event instanceof GlyphEvent) { + GlyphEvent glyphEvent = (GlyphEvent) event; + Glyph glyph = glyphEvent.getData(); + + evaluate(glyph); + } + } catch (Exception ex) { + logger.warn(sheet.getLogPrefix() + + getClass().getName() + " output error", ex); + } + } + + //--------------// + // defineLayout // + //--------------// + private void defineLayout () + { + String colSpec = Panel.makeColumns(3); + FormLayout layout = new FormLayout(colSpec, ""); + + int visibleButtons = Math.min( + constants.visibleButtons.getValue(), + selector.buttons.size()); + + for (int i = 0; i < visibleButtons; i++) { + layout.appendRow(FormSpecs.PREF_ROWSPEC); + } + ///SystemUtils.IS_OS_WINDOWS + + // Uncomment following line to have fixed sized rows, whether + // they are filled or not + ///layout.setRowGroups(new int[][]{{1, 3, 4, 5 }}); + PanelBuilder builder = new PanelBuilder(layout, getBody()); + builder.setDefaultDialogBorder(); + + CellConstraints cst = new CellConstraints(); + + for (int i = 0; i < visibleButtons; i++) { + int r = i + 1; // -------------------------------- + EvalButton evb = selector.buttons.get(i); + builder.add(evb.grade, cst.xy(5, r)); + builder.add( + (sheet != null) ? evb.button : evb.field, + cst.xyw(7, r, 5)); + } + } + + //~ Inner Classes ---------------------------------------------------------- + //-----------// + // Constants // + //-----------// + private static final class Constants + extends ConstantSet + { + //~ Instance fields ---------------------------------------------------- + + Evaluation.Grade minGrade = new Evaluation.Grade( + 0.0, + "Threshold on displayable grade"); + + Constant.Integer visibleButtons = new Constant.Integer( + "buttons", + 5, + "Max number of buttons in the shape selector"); + + } + + //------------// + // EvalButton // + //------------// + private class EvalButton + implements ActionListener + { + //~ Instance fields ---------------------------------------------------- + + // Shape button or text field. Only one of them will be created and used + final JButton button; + + final JLabel field; + + // The related grade + JLabel grade = new JLabel("", SwingConstants.RIGHT); + + //~ Constructors ------------------------------------------------------- + public EvalButton () + { + grade.setToolTipText("Grade of the evaluation"); + + if (sheet != null) { + button = new JButton(); + button.addActionListener(this); + button.setToolTipText("Assignable shape"); + button.setHorizontalAlignment(SwingConstants.LEFT); + field = null; + } else { + field = new JLabel(); + field.setHorizontalAlignment(JTextField.CENTER); + field.setToolTipText("Evaluated shape"); + field.setHorizontalAlignment(SwingConstants.LEFT); + button = null; + } + } + + //~ Methods ------------------------------------------------------------ + // Triggered by button + @Override + public void actionPerformed (ActionEvent e) + { + // Assign current glyph with selected shape + if (glyphsController != null) { + Glyph glyph = glyphsController.getNest() + .getSelectedGlyph(); + + if (glyph != null) { + String str = button.getText(); + Shape shape = Shape.valueOf(str); + + // Actually assign the shape + glyphsController.asyncAssignGlyphs( + Glyphs.sortedSet(glyph), + shape, + false); + } + } + } + + public void setEval (Evaluation eval, + boolean barred, + boolean enabled) + { + JComponent comp; + + if (sheet != null) { + comp = button; + } else { + comp = field; + } + + if (eval != null) { + Evaluation.Failure failure = eval.failure; + String text = eval.shape.toString(); + String tip = (failure != null) ? failure.toString() + : null; + + if (sheet != null) { + button.setEnabled(enabled); + + if (barred) { + button.setBackground(Colors.EVALUATION_BARRED); + } else { + button.setBackground(null); + } + + button.setText(text); + button.setToolTipText(tip); + button.setIcon(eval.shape.getDecoratedSymbol()); + } else { + if (barred) { + field.setBackground(Colors.EVALUATION_BARRED); + } else { + field.setBackground(null); + } + + field.setText(text); + field.setToolTipText(tip); + field.setIcon(eval.shape.getDecoratedSymbol()); + } + + comp.setVisible(true); + + if (failure == null) { + comp.setForeground(EVAL_GOOD_COLOR); + } else { + comp.setForeground(EVAL_SOSO_COLOR); + } + + grade.setVisible(true); + grade.setText(String.format("%.3f", eval.grade)); + } else { + grade.setVisible(false); + comp.setVisible(false); + } + } + } + + //----------// + // Selector // + //----------// + private class Selector + { + //~ Instance fields ---------------------------------------------------- + + // A collection of EvalButton's + final List buttons = new ArrayList<>(); + + //~ Constructors ------------------------------------------------------- + public Selector () + { + for (int i = 0; i < evalCount(); i++) { + buttons.add(new EvalButton()); + } + + setEvals(null, null); + } + + //~ Methods ------------------------------------------------------------ + //-----------// + // evalCount // + //-----------// + /** + * Report the number of displayed evaluations + * + * @return the number of eval buttons + */ + public final int evalCount () + { + return constants.visibleButtons.getValue(); + } + + //----------// + // setEvals // + //----------// + /** + * Display the evaluations with some text highlighting. + * Only first evalCount evaluations are displayed. + * + * @param evals top evaluations sorted from best to worst + * @param glyph evaluated glyph, to check forbidden shapes if any + */ + public final void setEvals (Evaluation[] evals, + Glyph glyph) + { + // Special case to empty the selector + if (evals == null) { + for (EvalButton evalButton : buttons) { + evalButton.setEval(null, false, false); + } + + return; + } + + boolean enabled = !glyph.isVirtual(); + double minGrade = constants.minGrade.getValue(); + int iBound = Math.min(evalCount(), evals.length); + int i; + + for (i = 0; i < iBound; i++) { + Evaluation eval = evals[i]; + + // Limitation on shape relevance + if (eval.grade < minGrade) { + break; + } + + // Barred on non-barred button + buttons.get(i) + .setEval( + eval, + glyph.isShapeForbidden(eval.shape), + enabled); + } + + // Zero the remaining buttons + for (; i < evalCount(); i++) { + buttons.get(i) + .setEval(null, false, false); + } + } + } +} diff --git a/src/main/omr/glyph/ui/GlyphBoard.java b/src/main/omr/glyph/ui/GlyphBoard.java new file mode 100644 index 0000000..5e95dcd --- /dev/null +++ b/src/main/omr/glyph/ui/GlyphBoard.java @@ -0,0 +1,596 @@ +//----------------------------------------------------------------------------// +// // +// G l y p h B o a r d // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.glyph.ui; + +import omr.constant.ConstantSet; + +import omr.glyph.Nest; +import omr.glyph.Shape; +import omr.glyph.facets.Glyph; + +import omr.selection.GlyphEvent; +import omr.selection.GlyphIdEvent; +import omr.selection.GlyphSetEvent; +import omr.selection.MouseMovement; +import omr.selection.SelectionHint; +import omr.selection.UserEvent; + +import omr.ui.Board; +import omr.ui.PixelCount; +import omr.ui.field.LTextField; +import omr.ui.field.SpinnerUtil; +import static omr.ui.field.SpinnerUtil.*; +import omr.ui.util.Panel; + +import omr.util.BasicTask; +import omr.util.Predicate; + +import com.jgoodies.forms.builder.PanelBuilder; +import com.jgoodies.forms.layout.CellConstraints; +import com.jgoodies.forms.layout.FormLayout; + +import org.jdesktop.application.Task; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.awt.Dimension; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Set; + +import javax.swing.AbstractAction; +import javax.swing.Action; +import javax.swing.JButton; +import javax.swing.JLabel; +import javax.swing.JSpinner; +import javax.swing.SwingConstants; +import javax.swing.event.ChangeEvent; +import javax.swing.event.ChangeListener; + +/** + * Class {@code GlyphBoard} defines a UI board dedicated to the display + * of {@link Glyph} information. + * + *

The universal globalSpinner addresses all glyphs + * currently defined in the nest (note that glyphs can be dynamically created or + * destroyed). + * + *

The spinner can be used to select a glyph by directly entering the + * glyph id value into the spinner field + * + * @author Hervé Bitteur + */ +public class GlyphBoard + extends Board + implements ChangeListener // For all spinners +{ + //~ Static fields/initializers --------------------------------------------- + + /** Specific application parameters */ + private static final Constants constants = new Constants(); + + /** Usual logger utility */ + private static final Logger logger = LoggerFactory.getLogger( + GlyphBoard.class); + + /** Events this board is interested in */ + private static final Class[] eventClasses = new Class[]{ + GlyphEvent.class, + GlyphSetEvent.class + }; + + /** Predicate for known glyphs */ + protected static final Predicate knownPredicate = new Predicate() + { + @Override + public boolean check (Glyph glyph) + { + return (glyph != null) && glyph.isKnown(); + } + }; + + //~ Instance fields -------------------------------------------------------- + /** The related glyph model */ + protected final GlyphsController controller; + + /** An active label */ + protected final JLabel active = new JLabel("", SwingConstants.CENTER); + + /** Input: Dump */ + protected final JButton dump; + + /** Counter of glyph selection */ + protected final JLabel count = new JLabel(""); + + /** Input : Deassign action */ + protected Action deassignAction; + + /** Output : glyph shape icon */ + protected final JLabel shapeIcon = new JLabel(); + + /** Input / Output : spinner of all glyphs */ + protected JSpinner globalSpinner; + + /** Input / Output : spinner of known glyphs */ + protected JSpinner knownSpinner; + + /** Output : shape of the glyph */ + protected final LTextField shapeField = new LTextField( + "", + "Assigned shape for this glyph"); + + /** The JGoodies/Form constraints to be used by all subclasses */ + protected final CellConstraints cst = new CellConstraints(); + + /** The JGoodies/Form layout to be used by all subclasses */ + protected final FormLayout layout = Panel.makeFormLayout(6, 3); + + /** The JGoodies/Form builder to be used by all subclasses */ + protected final PanelBuilder builder; + + /** + * We have to avoid endless loop, due to related modifications : When a + * GLYPH selection is notified, the id spinner is changed, and When an id + * spinner is changed, the GLYPH selection is notified + */ + protected boolean selfUpdating = false; + + //~ Constructors ----------------------------------------------------------- + //------------// + // GlyphBoard // + //------------// + /** + * Basic constructor, to set common characteristics. + * + * @param controller the related glyphs controller, if any + * @param useSpinners true for use of spinners + * @param expanded true if board must be initially expanded + */ + public GlyphBoard (GlyphsController controller, + boolean useSpinners, + boolean expanded) + { + super( + Board.GLYPH, + controller.getNest().getGlyphService(), + eventClasses, + true, // Dump + expanded); + + this.controller = controller; + + // Dump + dump = getDumpButton(); + dump.setToolTipText("Dump this glyph"); + dump.addActionListener( + new ActionListener() + { + @Override + public void actionPerformed (ActionEvent e) + { + // Retrieve current glyph selection + GlyphEvent glyphEvent = (GlyphEvent) getSelectionService() + .getLastEvent( + GlyphEvent.class); + Glyph glyph = glyphEvent.getData(); + + if (glyph != null) { + logger.info(glyph.dumpOf()); + } + } + }); + // Until a glyph selection is made + dump.setEnabled(false); + getDeassignAction() + .setEnabled(false); + + // Force a constant height for the shapeIcon field, despite the + // variation in size of the icon + Dimension dim = new Dimension( + constants.shapeIconWidth.getValue(), + constants.shapeIconHeight.getValue()); + shapeIcon.setPreferredSize(dim); + shapeIcon.setMaximumSize(dim); + shapeIcon.setMinimumSize(dim); + + // Precise layout + // layout.setColumnGroups( + // new int[][] { + // { 1, 5, 9 }, + // { 3, 7, 11 } + // }); + builder = new PanelBuilder(layout, getBody()); + builder.setDefaultDialogBorder(); + + defineLayout(); + + if (useSpinners) { + // Model for globalSpinner + globalSpinner = makeGlyphSpinner(controller.getNest(), null); + globalSpinner.setName("globalSpinner"); + globalSpinner.setToolTipText("General spinner for any glyph id"); + + // Layout + int r = 1; // -------------------------------- + + if (globalSpinner != null) { + builder.addLabel("Id", cst.xy(1, r)); + builder.add(globalSpinner, cst.xy(3, r)); + } + } + } + + //~ Methods ---------------------------------------------------------------- + //-------------------// + // getDeassignAction // + //-------------------// + /** + * Give access to the Deassign Action, to modify its properties + * + * @return the deassign action + */ + public Action getDeassignAction () + { + if (deassignAction == null) { + deassignAction = new DeassignAction(); + } + + return deassignAction; + } + + //---------// + // onEvent // + //---------// + /** + * Call-back triggered when Glyph Selection has been modified + * + * @param event of current glyph or glyph set + */ + @Override + public void onEvent (UserEvent event) + { + logger.debug("GlyphBoard event:{}", event); + + try { + // Ignore RELEASING + if (event.movement == MouseMovement.RELEASING) { + return; + } + + logger.debug( + "GlyphBoard selfUpdating={} : {}", + selfUpdating, + event); + + if (event instanceof GlyphEvent) { + // Display Glyph parameters (while preventing circular updates) + selfUpdating = true; + handleEvent((GlyphEvent) event); + selfUpdating = false; + } else if (event instanceof GlyphSetEvent) { + // Display count of glyphs in the glyph set + handleEvent((GlyphSetEvent) event); + } + } catch (Exception ex) { + logger.warn(getClass().getName() + " onEvent error", ex); + } + } + + //--------------// + // stateChanged // + //--------------// + /** + * CallBack triggered by a change in one of the spinners. + * + * @param e the change event, this allows to retrieve the originating + * spinner + */ + @Override + public void stateChanged (ChangeEvent e) + { + JSpinner spinner = (JSpinner) e.getSource(); + + // Nota: this method is automatically called whenever the spinner value + // is changed, including when a GLYPH selection notification is + // received leading to such selfUpdating. So the check. + if (!selfUpdating) { + // Notify the new glyph id + getSelectionService() + .publish( + new GlyphIdEvent( + this, + SelectionHint.GLYPH_INIT, + null, + (Integer) spinner.getValue())); + } + } + + //--------------// + // defineLayout // + //--------------// + /** + * Define the layout for common fields of all GlyphBoard classes + */ + protected void defineLayout () + { + int r = 1; // -------------------------------- + // Shape Icon (start, spans several rows) + count + active + Deassign button + + builder.add(shapeIcon, cst.xywh(1, r, 1, 5)); + + builder.add(count, cst.xy(5, r)); + + builder.add(active, cst.xy(7, r)); + + JButton deassignButton = new JButton(getDeassignAction()); + deassignButton.setHorizontalTextPosition(SwingConstants.LEFT); + deassignButton.setHorizontalAlignment(SwingConstants.RIGHT); + builder.add(deassignButton, cst.xyw(9, r, 3)); + + r += 2; // -------------------------------- + // Shape name + + builder.add(shapeField.getField(), cst.xyw(7, r, 5)); + } + + //------------------// + // makeGlyphSpinner // + //------------------// + /** + * Convenient method to allocate a glyph-based spinner + * + * @param nest the underlying glyph nest + * @param predicate a related glyph predicate, if any + * @return the spinner built + */ + protected JSpinner makeGlyphSpinner (Nest nest, + Predicate predicate) + { + JSpinner spinner = new JSpinner(); + spinner.setModel(new SpinnerGlyphModel(nest, predicate)); + spinner.addChangeListener(this); + SpinnerUtil.setRightAlignment(spinner); + SpinnerUtil.setEditable(spinner, true); + + return spinner; + } + + //-------------// + // handleEvent // + //-------------// + /** + * Interest in Glyph + * + * @param GlyphEvent + */ + private void handleEvent (GlyphEvent glyphEvent) + { + // Display Glyph parameters + Glyph glyph = glyphEvent.getData(); + + // Active ? + if (glyph != null) { + if (glyph.isActive()) { + if (glyph.isVirtual()) { + active.setText("Virtual"); + } else { + active.setText("Active"); + } + } else { + active.setText("Non Active"); + } + } else { + active.setText(""); + } + + // Dump button and deassign button + dump.setEnabled(glyph != null); + getDeassignAction() + .setEnabled((glyph != null) && glyph.isKnown()); + + // Shape text and icon + Shape shape = (glyph != null) ? glyph.getShape() : null; + + if (shape != null) { + if ((shape == Shape.GLYPH_PART) && (glyph.getPartOf() != null)) { + shapeField.setText(shape + " of #" + glyph.getPartOf().getId()); + } else { + shapeField.setText(shape.toString()); + } + + shapeIcon.setIcon(shape.getDecoratedSymbol()); + } else { + shapeField.setText(""); + shapeIcon.setIcon(null); + } + + // Global Spinner + if (globalSpinner != null) { + if (glyph != null) { + globalSpinner.setValue(glyph.getId()); + } else { + globalSpinner.setValue(NO_VALUE); + } + } + + // Known Spinner + if (knownSpinner != null) { + if (glyph != null) { + knownSpinner.setValue( + knownPredicate.check(glyph) ? glyph.getId() : NO_VALUE); + } else { + knownSpinner.setValue(NO_VALUE); + } + } + } + + //-------------// + // handleEvent // + //-------------// + /** + * Interest in GlyphSet + * + * @param GlyphSetEvent + */ + private void handleEvent (GlyphSetEvent glyphSetEvent) + { + // Display count of glyphs in the glyph set + Set glyphs = glyphSetEvent.getData(); + + if ((glyphs != null) && !glyphs.isEmpty()) { + count.setText(Integer.toString(glyphs.size())); + } else { + count.setText(""); + } + } + + //~ Inner Classes ---------------------------------------------------------- + //-----------// + // Constants // + //-----------// + private static final class Constants + extends ConstantSet + { + //~ Instance fields ---------------------------------------------------- + + /** Exact pixel height for the shape icon field */ + PixelCount shapeIconHeight = new PixelCount( + 70, + "Exact pixel height for the shape icon field"); + + /** Exact pixel width for the shape icon field */ + PixelCount shapeIconWidth = new PixelCount( + 50, + "Exact pixel width for the shape icon field"); + + } + + //----------------// + // DeassignAction // + //----------------// + private class DeassignAction + extends AbstractAction + { + //~ Constructors ------------------------------------------------------- + + public DeassignAction () + { + super("Deassign"); + this.putValue(Action.SHORT_DESCRIPTION, "Deassign shape"); + } + + //~ Methods ------------------------------------------------------------ + @SuppressWarnings("unchecked") + @Override + public void actionPerformed (ActionEvent e) + { + List> classes = Arrays.asList(eventClasses); + + if ((controller != null) && !classes.isEmpty()) { + // Do we have selections for glyph set, or just for glyph? + if (classes.contains(GlyphEvent.class)) { + final Glyph glyph = (Glyph) getSelectionService() + .getSelection( + GlyphEvent.class); + + if (classes.contains(GlyphSetEvent.class)) { + final Set glyphs = (Set) getSelectionService() + .getSelection( + GlyphSetEvent.class); + + boolean noVirtuals = true; + + for (Glyph g : glyphs) { + if (g.isVirtual()) { + noVirtuals = false; + + break; + } + } + + if (noVirtuals) { + new BasicTask() + { + @Override + protected Void doInBackground () + throws Exception + { + // Following actions must be done in sequence + Task task = controller.asyncDeassignGlyphs( + glyphs); + + if (task != null) { + task.get(); + + // Update focus on current glyph, + // even if reused in a compound + Glyph newGlyph = glyph.getFirstSection() + .getGlyph(); + getSelectionService() + .publish( + new GlyphEvent( + this, + SelectionHint.GLYPH_INIT, + null, + newGlyph)); + } + + return null; + } + }.execute(); + } else { + new BasicTask() + { + @Override + protected Void doInBackground () + throws Exception + { + // Following actions must be done in sequence + Task task = controller.asyncDeleteVirtualGlyphs( + glyphs); + + if (task != null) { + task.get(); + + // Null publication + getSelectionService() + .publish( + new GlyphEvent( + this, + SelectionHint.GLYPH_INIT, + null, + null)); + } + + return null; + } + }.execute(); + } + } else { + // We have selection for glyph only + if (glyph.isVirtual()) { + controller.asyncDeleteVirtualGlyphs( + Collections.singleton(glyph)); + } else { + controller.asyncDeassignGlyphs( + Collections.singleton(glyph)); + } + } + } + } + } + } +} diff --git a/src/main/omr/glyph/ui/GlyphBrowser.java b/src/main/omr/glyph/ui/GlyphBrowser.java new file mode 100644 index 0000000..3797459 --- /dev/null +++ b/src/main/omr/glyph/ui/GlyphBrowser.java @@ -0,0 +1,927 @@ +//----------------------------------------------------------------------------// +// // +// G l y p h B r o w s e r // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.glyph.ui; + +import omr.WellKnowns; + +import omr.constant.Constant; +import omr.constant.ConstantSet; + +import omr.glyph.BasicNest; +import omr.glyph.GlyphRepository; +import omr.glyph.GlyphSignature; +import omr.glyph.GlyphsModel; +import omr.glyph.Nest; +import omr.glyph.facets.Glyph; + +import omr.lag.BasicLag; +import omr.lag.Lag; +import omr.lag.Section; + +import omr.run.Orientation; + +import omr.selection.GlyphEvent; +import omr.selection.LocationEvent; +import omr.selection.MouseMovement; +import omr.selection.SelectionHint; +import static omr.selection.SelectionHint.*; +import omr.selection.SelectionService; +import omr.selection.UserEvent; + +import omr.sheet.Sheet; + +import omr.ui.Board; +import omr.ui.field.LTextField; +import omr.ui.util.Panel; +import omr.ui.view.LogSlider; +import omr.ui.view.Rubber; +import omr.ui.view.ScrollView; +import omr.ui.view.Zoom; + +import omr.util.BlackList; + +import com.jgoodies.forms.builder.PanelBuilder; +import com.jgoodies.forms.layout.CellConstraints; +import com.jgoodies.forms.layout.FormLayout; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.awt.BorderLayout; +import java.awt.Color; +import java.awt.Dimension; +import java.awt.Graphics2D; +import java.awt.Rectangle; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.io.File; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import javax.swing.AbstractAction; +import javax.swing.Action; +import javax.swing.JButton; +import javax.swing.JLabel; +import javax.swing.JOptionPane; +import javax.swing.JPanel; +import javax.swing.JTextField; +import javax.swing.SwingConstants; +import javax.swing.event.ChangeEvent; +import javax.swing.event.ChangeListener; + +/** + * Class {@code GlyphBrowser} gathers a navigator to move between + * selected glyphs, a glyph board for glyph details, and a display for + * graphical glyph view. + * This is a (package private) companion of {@link SampleVerifier}. + * + * @author Hervé Bitteur + */ +class GlyphBrowser + implements ChangeListener +{ + //~ Static fields/initializers --------------------------------------------- + + /** Specific application parameters */ + private static final Constants constants = new Constants(); + + /** Usual logger utility */ + private static final Logger logger = LoggerFactory.getLogger( + GlyphBrowser.class); + + /** Events that can be published on internal service (TODO: Check this!) */ + private static final Class[] locEvents = new Class[]{ + LocationEvent.class + }; + + /** + * Field constant {@code NO_INDEX} is a specific value {@value} to + * indicate absence of index + */ + private static final int NO_INDEX = -1; + + //~ Instance fields -------------------------------------------------------- + /** The concrete Swing component */ + private JPanel component = new JPanel(); + + /** Reference of SampleVerifier */ + private final SampleVerifier verifier; + + /** Repository of known glyphs */ + private final GlyphRepository repository = GlyphRepository.getInstance(); + + /** Size of the lag display */ + private Dimension modelSize; + + /** Contour of the lag display */ + private Rectangle modelRectangle; + + /** Population of glyphs file names */ + private List names = Collections.emptyList(); + + /** Navigator instance to navigate through all glyphs names */ + private Navigator navigator; + + /** Left panel : navigator, glyphboard, evaluator */ + private JPanel leftPanel; + + /** Composite display (view + zoom slider) */ + private Display display; + + /** Basic location event service */ + private SelectionService locationService; + + /** Hosting Nest */ + private Nest tNest; + + /** Vertical Lag */ + private Lag vtLag; + + /** Horizontal Lag */ + private Lag htLag; + + /** Basic glyph model */ + private GlyphsController controller; + + /** The glyph view */ + private NestView view; + + /** Glyph board with ability to delete a training glyph */ + private GlyphBoard glyphBoard; + + //~ Constructors ----------------------------------------------------------- + //--------------// + // GlyphBrowser // + //--------------// + /** + * Create an instance, with back-reference to SampleVerifier. + * + * @param verifier ref back to verifier + */ + public GlyphBrowser (SampleVerifier verifier) + { + this.verifier = verifier; + + // Layout + component.setLayout(new BorderLayout()); + resetBrowser(); + } + + //~ Methods ---------------------------------------------------------------- + //--------------// + // getComponent // + //--------------// + /** + * Report the UI component. + * + * @return the concrete component + */ + public JPanel getComponent () + { + return component; + } + + //----------------// + // loadGlyphNames // + //----------------// + /** + * Programmatic use of Load action in Navigator: load the glyph names as + * selected, and focus on first glyph. + */ + public void loadGlyphNames () + { + navigator.loadAction.actionPerformed(null); + } + + //--------------// + // stateChanged // + //--------------// + /** + * Called when a new selection has been made in SampleVerifier + * companion. + * + * @param e not used + */ + @Override + public void stateChanged (ChangeEvent e) + { + int selNb = verifier.getGlyphCount(); + navigator.loadAction.setEnabled(selNb > 0); + } + + //----------------// + // buildLeftPanel // + //----------------// + /** + * Build a panel composed vertically of a Navigator, a GlyphBoard + * and an EvaluationBoard. + * + * @return the UI component, ready to be inserted in Swing hierarchy + */ + private JPanel buildLeftPanel () + { + navigator = new Navigator(); + + // Specific glyph board + glyphBoard = new MyGlyphBoard(controller); + + glyphBoard.connect(); + glyphBoard.getDeassignAction() + .setEnabled(false); + + // Passive evaluation board + EvaluationBoard evalBoard = new EvaluationBoard(controller, true); + evalBoard.connect(); + + // Layout + FormLayout layout = new FormLayout("pref", "pref,pref,pref"); + PanelBuilder builder = new PanelBuilder(layout); + CellConstraints cst = new CellConstraints(); + builder.setDefaultDialogBorder(); + + builder.add(navigator.getComponent(), cst.xy(1, 1)); + builder.add(glyphBoard.getComponent(), cst.xy(1, 2)); + builder.add(evalBoard.getComponent(), cst.xy(1, 3)); + + return builder.getPanel(); + } + + //-------------// + // removeGlyph // + //-------------// + private void removeGlyph () + { + int index = navigator.getIndex(); + + if (index >= 0) { + // Delete glyph designated by index + String gName = names.get(index); + Glyph glyph = navigator.getGlyph(gName); + + // User confirmation is required ? + if (constants.confirmDeletions.getValue()) { + if (JOptionPane.showConfirmDialog( + component, + "Remove glyph '" + gName + "' ?") != JOptionPane.YES_OPTION) { + return; + } + } + + // Shrink names list + names.remove(index); + + // Update model & display + repository.removeGlyph(gName); + + for (Section section : glyph.getMembers()) { + section.delete(); + } + + // Update the Glyph selector also ! + verifier.deleteGlyphName(gName); + + // Perform file deletion + if (repository.isIcon(gName)) { + new SymbolsBlackList().add(new File(gName)); + } else { + File file = new File(WellKnowns.TRAIN_FOLDER, gName); + new BlackList(file.getParentFile()).add(new File(gName)); + } + + logger.info("Removed {}", gName); + + // Set new index ? + if (index < names.size()) { + navigator.setIndex(index, GLYPH_INIT); // Next + } else { + navigator.setIndex(index - 1, GLYPH_INIT); // Prev/None + } + } else { + logger.warn("No selected glyph to remove!"); + } + } + + //--------------// + // resetBrowser // + //--------------// + private void resetBrowser () + { + // Reset model + tNest = new NoSigNest("tNest", null); + htLag = new BasicLag("htLag", Orientation.HORIZONTAL); + vtLag = new BasicLag("vtLag", Orientation.VERTICAL); + + locationService = new SelectionService("BrowserLocation", locEvents); + controller = new BasicController(tNest, locationService); + tNest.setServices(locationService); + + // Reset left panel + if (leftPanel != null) { + component.remove(leftPanel); + } + + leftPanel = buildLeftPanel(); + component.add(leftPanel, BorderLayout.WEST); + + // Reset display + if (display != null) { + component.remove(display); + } + + display = new Display(); + component.add(display, BorderLayout.CENTER); + + // TODO: Check if all this is really needed ... + component.invalidate(); + component.validate(); + component.repaint(); + } + + //~ Inner Classes ---------------------------------------------------------- + //-----------------// + // BasicController // + //-----------------// + /** + * A very basic glyphs controller, with a sheet-less location service. + */ + private class BasicController + extends GlyphsController + { + //~ Instance fields ---------------------------------------------------- + + /** A specific location service, not tied to a sheet */ + private final SelectionService locationService; + + //~ Constructors ------------------------------------------------------- + public BasicController (Nest nest, + SelectionService locationService) + { + super(new BasicModel(nest)); + this.locationService = locationService; + } + + //~ Methods ------------------------------------------------------------ + @Override + public SelectionService getLocationService () + { + return this.locationService; + } + } + + //------------// + // BasicModel // + //------------// + /** + * A very basic glyphs model, used to handle the deletion of glyphs. + */ + private class BasicModel + extends GlyphsModel + { + //~ Constructors ------------------------------------------------------- + + public BasicModel (Nest nest) + { + super(null, nest, null); + } + + //~ Methods ------------------------------------------------------------ + // Certainly not called ... + @Override + public void deassignGlyph (Glyph glyph) + { + removeGlyph(); + } + } + + //-----------// + // Constants // + //-----------// + private static final class Constants + extends ConstantSet + { + //~ Instance fields ---------------------------------------------------- + + Constant.Boolean confirmDeletions = new Constant.Boolean( + true, + "Should user confirm each glyph deletion" + + " from training material"); + + } + + //----------------// + // DeassignAction // + //----------------// + private class DeassignAction + extends AbstractAction + { + //~ Constructors ------------------------------------------------------- + + public DeassignAction () + { + super("Remove"); + putValue( + Action.SHORT_DESCRIPTION, + "Remove that glyph from training material"); + } + + //~ Methods ------------------------------------------------------------ + @SuppressWarnings("unchecked") + @Override + public void actionPerformed (ActionEvent e) + { + removeGlyph(); + } + } + + //---------// + // Display // + //---------// + private class Display + extends JPanel + { + //~ Instance fields ---------------------------------------------------- + + LogSlider slider; + + Rubber rubber; + + ScrollView slv; + + Zoom zoom; + + //~ Constructors ------------------------------------------------------- + public Display () + { + view = new MyView(controller); + view.setLocationService(locationService); + view.subscribe(); + modelRectangle = new Rectangle(); + modelSize = new Dimension(0, 0); + slider = new LogSlider(2, 5, LogSlider.VERTICAL, -3, 4, 0); + zoom = new Zoom(slider, 1); // Default ratio set to 1 + rubber = new Rubber(view, zoom); + rubber.setMouseMonitor(view); + view.setZoom(zoom); + view.setRubber(rubber); + slv = new ScrollView(view); + + // Layout + setLayout(new BorderLayout()); + add(slider, BorderLayout.WEST); + add(slv.getComponent(), BorderLayout.CENTER); + } + } + + //------------// + // LoadAction // + //------------// + private class LoadAction + extends AbstractAction + { + //~ Constructors ------------------------------------------------------- + + public LoadAction () + { + super("Load"); + } + + //~ Methods ------------------------------------------------------------ + @Override + public void actionPerformed (ActionEvent e) + { + // Get a (shrinkable, to allow deletions) list of glyph names + names = verifier.getGlyphNames(); + + // Reset lag & display + resetBrowser(); + + // Set navigator on first glyph, if any + if (!names.isEmpty()) { + navigator.setIndex(0, GLYPH_INIT); + } else { + if (e != null) { + logger.warn("No glyphs selected in Glyph Selector"); + } + + navigator.all.setEnabled(false); + navigator.prev.setEnabled(false); + navigator.next.setEnabled(false); + } + } + } + + //--------------// + // MyGlyphBoard // + //--------------// + private class MyGlyphBoard + extends SymbolGlyphBoard + { + //~ Constructors ------------------------------------------------------- + + public MyGlyphBoard (GlyphsController controller) + { + super(controller, false, true); + } + + //~ Methods ------------------------------------------------------------ + @Override + public Action getDeassignAction () + { + if (deassignAction == null) { + deassignAction = new DeassignAction(); + } + + return deassignAction; + } + } + + //--------// + // MyView // + //--------// + private final class MyView + extends NestView + { + //~ Constructors ------------------------------------------------------- + + public MyView (GlyphsController controller) + { + super(tNest, controller, Arrays.asList(htLag, vtLag)); + setName("GlyphBrowser-View"); + subscribe(); + } + + //~ Methods ------------------------------------------------------------ + //---------// + // onEvent // + //---------// + /** + * Call-back triggered from (local) selection objects. + * + * @param event the notified event + */ + @Override + public void onEvent (UserEvent event) + { + try { + // Ignore RELEASING + if (event.movement == MouseMovement.RELEASING) { + return; + } + + // Keep normal view behavior (rubber, etc...) + super.onEvent(event); + + // Additional tasks + if (event instanceof LocationEvent) { + LocationEvent sheetLocation = (LocationEvent) event; + + if (sheetLocation.hint == SelectionHint.LOCATION_INIT) { + Rectangle rect = sheetLocation.getData(); + + if ((rect != null) + && (rect.width == 0) + && (rect.height == 0)) { + // Look for pointed glyph + int index = glyphLookup(rect); + navigator.setIndex(index, sheetLocation.hint); + } + } + } else if (event instanceof GlyphEvent) { + GlyphEvent glyphEvent = (GlyphEvent) event; + + if (glyphEvent.hint == GLYPH_INIT) { + Glyph glyph = glyphEvent.getData(); + + // Display glyph contour + if (glyph != null) { + locationService.publish( + new LocationEvent( + this, + glyphEvent.hint, + null, + glyph.getBounds())); + } + } + } + } catch (Exception ex) { + logger.warn(getClass().getName() + " onEvent error", ex); + } + } + + //-------------// + // renderItems // + //-------------// + @Override + public void renderItems (Graphics2D g) + { + // Mark the current glyph + int index = navigator.getIndex(); + + if (index >= 0) { + String gName = names.get(index); + Glyph glyph = navigator.getGlyph(gName); + g.setColor(Color.black); + g.setXORMode(Color.darkGray); + renderGlyphArea(glyph, g); + } + } + + //-------------// + // glyphLookup // + //-------------// + /** + * Lookup for a glyph that is pointed by rectangle location. This is a + * very specific glyph lookup, for which we cannot rely on Nest + * usual features. So we simply browse through the collection of glyphs + * (names). + * + * @param rect location (upper left corner) + * @return index in names collection if found, NO_INDEX otherwise + */ + private int glyphLookup (Rectangle rect) + { + int index = -1; + + for (String gName : names) { + index++; + + if (repository.isLoaded(gName)) { + Glyph glyph = navigator.getGlyph(gName); + + if (glyph.getNest() == tNest) { + for (Section section : glyph.getMembers()) { + if (section.contains(rect.x, rect.y)) { + return index; + } + } + } + } + } + + return NO_INDEX; // Not found + } + } + + //-----------// + // Navigator // + //-----------// + /** + * Class {@code Navigator} handles the navigation through the + * collection of glyphs (names). + */ + private final class Navigator + extends Board + { + //~ Instance fields ---------------------------------------------------- + + /** Current index in names collection (NO_INDEX if none) */ + private int nameIndex = NO_INDEX; + + // Navigation actions & buttons + LoadAction loadAction = new LoadAction(); + + JButton load = new JButton(loadAction); + + JButton all = new JButton("All"); + + JButton next = new JButton("Next"); + + JButton prev = new JButton("Prev"); + + LTextField nameField = new LTextField("", "File where glyph is stored"); + + //~ Constructors ------------------------------------------------------- + //-----------// + // Navigator // + //-----------// + Navigator () + { + super(Board.SAMPLE, null, null, false, true); + + defineLayout(); + + all.addActionListener( + new ActionListener() + { + @Override + public void actionPerformed (ActionEvent e) + { + // Load all (non icon) glyphs + int index = -1; + + for (String gName : names) { + index++; + + if (!repository.isIcon(gName)) { + setIndex(index, GLYPH_INIT); + } + } + + // Load & point to first icon + setIndex(0, GLYPH_INIT); + } + }); + + prev.addActionListener( + new ActionListener() + { + @Override + public void actionPerformed (ActionEvent e) + { + setIndex(nameIndex - 1, GLYPH_INIT); // To prev + } + }); + + next.addActionListener( + new ActionListener() + { + @Override + public void actionPerformed (ActionEvent e) + { + setIndex(nameIndex + 1, GLYPH_INIT); // To next + } + }); + + load.setToolTipText("Load the selected glyphs"); + all.setToolTipText("Display all glyphs"); + prev.setToolTipText("Go to previous glyph"); + next.setToolTipText("Go to next glyph"); + + loadAction.setEnabled(false); + all.setEnabled(false); + prev.setEnabled(false); + next.setEnabled(false); + } + + //~ Methods ------------------------------------------------------------ + //----------// + // getGlyph // + //----------// + public Glyph getGlyph (String gName) + { + Glyph glyph = repository.getGlyph(gName, null); + + if (glyph == null) { + return null; + } + + if (glyph.getNest() != tNest) { + tNest.addGlyph(glyph); + + Color color = glyph.getShape() + .getColor(); + + for (Section section : glyph.getMembers()) { + Lag lag = section.isVertical() ? vtLag : htLag; + + lag.addVertex(section); // Trick! + section.setGraph(lag); + + section.setColor(color); + } + } + + return glyph; + } + + //----------// + // getIndex // + //----------// + /** + * Report the current glyph index in the names collection. + * + * @return the current index, which may be NO_INDEX + */ + public final int getIndex () + { + return nameIndex; + } + + // Just to please the Board interface + @Override + public void onEvent (UserEvent event) + { + throw new UnsupportedOperationException("Not supported yet."); + } + + //----------// + // setIndex // + //----------// + /** + * Only method allowed to designate a glyph. + * + * @param index index of new current glyph + * @param hint related processing hint + */ + public void setIndex (int index, + SelectionHint hint) + { + Glyph glyph = null; + + if (index >= 0) { + String gName = names.get(index); + nameField.setText(gName); + + // Special case for icon : if we point to an icon, we have to + // get rid of all other icons (standard glyphs can be kept) + // Otherwise, they would all be displayed on top of the other + if (repository.isIcon(gName)) { + repository.unloadIconsFrom(names); + } + + // Load the desired glyph if needed + glyph = getGlyph(gName); + + if (glyph == null) { + return; + } + + // Extend view model size if needed + Rectangle box = glyph.getBounds(); + modelRectangle = modelRectangle.union(box); + + Dimension newSize = modelRectangle.getSize(); + + if (!newSize.equals(modelSize)) { + modelSize = newSize; + view.setModelSize(modelSize); + } + } else { + nameField.setText(""); + } + + nameIndex = index; + + tNest.getGlyphService() + .publish(new GlyphEvent(this, hint, null, glyph)); + + // Enable buttons according to glyph selection + all.setEnabled(!names.isEmpty()); + prev.setEnabled(index > 0); + next.setEnabled((index >= 0) && (index < (names.size() - 1))); + } + + //--------------// + // defineLayout // + //--------------// + private void defineLayout () + { + CellConstraints cst = new CellConstraints(); + FormLayout layout = Panel.makeFormLayout(4, 3); + PanelBuilder builder = new PanelBuilder(layout, super.getBody()); + builder.setDefaultDialogBorder(); + + int r = 1; // -------------------------------- + builder.add(load, cst.xy(11, r)); + + r += 2; // -------------------------------- + builder.add(all, cst.xy(3, r)); + builder.add(prev, cst.xy(7, r)); + builder.add(next, cst.xy(11, r)); + + r += 2; // -------------------------------- + + JLabel file = new JLabel("File", SwingConstants.RIGHT); + builder.add(file, cst.xy(1, r)); + + nameField.getField() + .setHorizontalAlignment(JTextField.LEFT); + builder.add(nameField.getField(), cst.xyw(3, r, 9)); + } + } + + //-----------// + // NoSigNest // + //-----------// + /** + * A specific glyph nest, with no handling of signature. + */ + private static class NoSigNest + extends BasicNest + { + //~ Constructors ------------------------------------------------------- + + public NoSigNest (String name, + Sheet sheet) + { + super(name, sheet); + } + + //~ Methods ------------------------------------------------------------ + @Override + public Glyph getOriginal (GlyphSignature signature) + { + return null; + } + } +} diff --git a/src/main/omr/glyph/ui/GlyphMenu.java b/src/main/omr/glyph/ui/GlyphMenu.java new file mode 100644 index 0000000..610b919 --- /dev/null +++ b/src/main/omr/glyph/ui/GlyphMenu.java @@ -0,0 +1,704 @@ +//----------------------------------------------------------------------------// +// // +// G l y p h M e n u // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.glyph.ui; + +import omr.glyph.Nest; +import omr.glyph.Shape; +import omr.glyph.ShapeSet; +import omr.glyph.facets.Glyph; + +import omr.selection.GlyphEvent; +import omr.selection.SelectionHint; + +import omr.sheet.Sheet; + +import omr.ui.util.SeparableMenu; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import java.util.SortedSet; +import java.util.TreeSet; + +import javax.swing.AbstractAction; +import javax.swing.JMenu; +import javax.swing.JMenuItem; + +/** + * Abstract class {@code GlyphMenu} is the base for glyph-based + * menus such as {@link SymbolMenu}. + * It also provides implementation for basic actions: copy, paste, assign, + * compound, deassign and dump. + * + *

In a menu, actions are physically grouped by semantic tag and separators + * are inserted between such groups.

+ * + *

Actions are also organized according to their target menu level, to + * allow actions to be dispatched into a hierarchy of menus. + * Although currently all levels are set to 0.

+ * + * @author Hervé Bitteur + */ +public abstract class GlyphMenu +{ + //~ Static fields/initializers --------------------------------------------- + + /** Usual logger utility */ + private static final Logger logger = LoggerFactory.getLogger(GlyphMenu.class); + + //~ Instance fields -------------------------------------------------------- + /** Map action -> tag to update according to context */ + private final Map dynActions = new LinkedHashMap<>(); + + /** Map action -> menu level */ + private final Map levels = new LinkedHashMap<>(); + + /** Concrete menu */ + private final SeparableMenu menu = new SeparableMenu(); + + /** The controller in charge of user gesture */ + protected final GlyphsController controller; + + /** Related sheet */ + protected final Sheet sheet; + + /** Related nest */ + protected final Nest nest; + + /** Current number of selected glyphs */ + protected int glyphNb; + + /** Current number of known glyphs */ + protected int knownNb; + + /** Current number of stems */ + protected int stemNb; + + /** Current number of virtual glyphs */ + protected int virtualNb; + + /** Sure we have no virtual glyphs? */ + protected boolean noVirtuals; + + //~ Constructors ----------------------------------------------------------- + //-----------// + // GlyphMenu // + //-----------// + /** + * Creates a new GlyphMenu object. + * + * @param controller the related glyphs controller + */ + public GlyphMenu (GlyphsController controller) + { + this.controller = controller; + + sheet = controller.sheet; + nest = controller.getNest(); + + buildMenu(); + } + + //~ Methods ---------------------------------------------------------------- + //---------// + // getMenu // + //---------// + /** + * Report the concrete menu. + * + * @return the menu + */ + public JMenu getMenu () + { + return menu; + } + + //------------// + // updateMenu // + //------------// + /** + * Update the menu according to the currently selected glyphs. + * + * @return the number of selected glyphs + */ + public int updateMenu () + { + // Analyze the context + glyphNb = 0; + knownNb = 0; + stemNb = 0; + virtualNb = 0; + + Set glyphs = nest.getSelectedGlyphSet(); + + if (glyphs != null) { + glyphNb = glyphs.size(); + + for (Glyph glyph : glyphs) { + if (glyph.isKnown()) { + knownNb++; + + if (glyph.getShape() == Shape.STEM) { + stemNb++; + } + } + + if (glyph.isVirtual()) { + virtualNb++; + } + } + } + + noVirtuals = (virtualNb == 0); + + // Update all dynamic actions accordingly + for (DynAction action : dynActions.keySet()) { + action.update(); + } + + // Update the menu root item + menu.setEnabled(glyphNb > 0); + + if (glyphNb > 0) { + menu.setText("Glyphs ..."); + } else { + menu.setText("no glyph"); + } + + return glyphNb; + } + + //-----------------// + // registerActions // + //-----------------// + /** + * Register all actions to be used in the menu + */ + protected abstract void registerActions (); + + //----------// + // register // + //----------// + /** + * Register this action instance in the set of dynamic actions + * + * @param menuLevel which menu should host the action item + * @param action the action to register + */ + protected void register (int menuLevel, + DynAction action) + { + levels.put(action, menuLevel); + dynActions.put(action, action.tag); + } + + //-----------// + // buildMenu // + //-----------// + /** + * Build the menu instance, grouping the actions with the same tag + * and separating them from other tags, and organize actions into + * their target menu level. + */ + private void buildMenu () + { + // Register actions + registerActions(); + + // Sort actions on their tag + SortedSet tags = new TreeSet<>(dynActions.values()); + + // Retrieve the highest menu level + int maxLevel = 0; + + for (Integer level : levels.values()) { + maxLevel = Math.max(maxLevel, level); + } + + // Initially update all the action items + for (DynAction action : dynActions.keySet()) { + action.update(); + } + + // Generate the hierarchy of menus + SeparableMenu prevMenu = menu; + + for (int level = 0; level <= maxLevel; level++) { + SeparableMenu currentMenu = (level == 0) ? menu + : new SeparableMenu("Continued ..."); + + for (Integer tag : tags) { + for (Entry entry : dynActions.entrySet()) { + if (entry.getValue() + .equals(tag)) { + DynAction action = entry.getKey(); + + if (levels.get(action) == level) { + currentMenu.add(action.getMenuItem()); + } + } + } + + currentMenu.addSeparator(); + } + + currentMenu.trimSeparator(); + + if ((level > 0) && (currentMenu.getMenuComponentCount() > 0)) { + // Insert this menu as a submenu of the previous one + prevMenu.addSeparator(); + prevMenu.add(currentMenu); + prevMenu = currentMenu; + } + } + } + + //~ Inner Classes ---------------------------------------------------------- + //----------------// + // AssignListener // + //----------------// + /** + * A standard listener used in all shape assignment menus. + */ + protected class AssignListener + implements ActionListener + { + //~ Instance fields ---------------------------------------------------- + + private final boolean compound; + + //~ Constructors ------------------------------------------------------- + /** + * Creates the AssignListener, with the compound flag. + * + * @param compound true if we assign a compound, false otherwise + */ + public AssignListener (boolean compound) + { + this.compound = compound; + } + + //~ Methods ------------------------------------------------------------ + @Override + public void actionPerformed (ActionEvent e) + { + JMenuItem source = (JMenuItem) e.getSource(); + controller.asyncAssignGlyphs( + nest.getSelectedGlyphSet(), + Shape.valueOf(source.getText()), + compound); + } + } + + //----------------// + // CompoundAction // + //----------------// + /** + * Build a compound and assign the shape selected in the menu. + */ + protected class CompoundAction + extends DynAction + { + //~ Constructors ------------------------------------------------------- + + public CompoundAction () + { + super(30); + } + + //~ Methods ------------------------------------------------------------ + @Override + public void actionPerformed (ActionEvent e) + { + // Default action is to open the menu + assert false; + } + + @Override + public JMenuItem getMenuItem () + { + JMenu menu = new JMenu(this); + ShapeSet.addAllShapes(menu, new AssignListener(true)); + + return menu; + } + + @Override + public void update () + { + if ((glyphNb > 1) && noVirtuals) { + setEnabled(true); + putValue(NAME, "Build compound as ..."); + putValue(SHORT_DESCRIPTION, "Manually build a compound"); + } else { + setEnabled(false); + putValue(NAME, "No compound"); + putValue(SHORT_DESCRIPTION, "No glyphs for a compound"); + } + } + } + + //------------// + // CopyAction // + //------------// + /** + * Copy the shape of the selected glyph shape (in order to replicate + * the assignment to another glyph later). + */ + protected class CopyAction + extends DynAction + { + //~ Constructors ------------------------------------------------------- + + public CopyAction () + { + super(10); + } + + //~ Methods ------------------------------------------------------------ + @Override + public void actionPerformed (ActionEvent e) + { + Glyph glyph = nest.getSelectedGlyph(); + + if (glyph != null) { + Shape shape = glyph.getShape(); + + if (shape != null) { + controller.setLatestShapeAssigned(shape); + } + } + } + + @Override + public void update () + { + Glyph glyph = nest.getSelectedGlyph(); + + if (glyph != null) { + Shape shape = glyph.getShape(); + + if (shape != null) { + setEnabled(true); + putValue(NAME, "Copy " + shape); + putValue(SHORT_DESCRIPTION, "Copy this shape"); + + return; + } + } + + setEnabled(false); + putValue(NAME, "Copy"); + putValue(SHORT_DESCRIPTION, "No shape to copy"); + } + } + + //------------// + // DumpAction // + //------------// + /** + * Dump each glyph in the selected collection of glyphs. + */ + protected class DumpAction + extends DynAction + { + //~ Constructors ------------------------------------------------------- + + public DumpAction () + { + super(40); + } + + //~ Methods ------------------------------------------------------------ + @Override + public void actionPerformed (ActionEvent e) + { + for (Glyph glyph : nest.getSelectedGlyphSet()) { + logger.info(glyph.dumpOf()); + } + } + + @Override + public void update () + { + if (glyphNb > 0) { + setEnabled(true); + + StringBuilder sb = new StringBuilder(); + sb.append("Dump ") + .append(glyphNb) + .append(" glyph"); + + if (glyphNb > 1) { + sb.append("s"); + } + + putValue(NAME, sb.toString()); + putValue(SHORT_DESCRIPTION, "Dump selected glyphs"); + } else { + setEnabled(false); + putValue(NAME, "Dump"); + putValue(SHORT_DESCRIPTION, "No glyph to dump"); + } + } + } + + //-----------// + // DynAction // + //-----------// + /** + * Base implementation, to register the dynamic actions that need + * to be updated according to the current glyph selection context. + */ + protected abstract class DynAction + extends AbstractAction + { + //~ Instance fields ---------------------------------------------------- + + /** Semantic tag */ + protected final int tag; + + //~ Constructors ------------------------------------------------------- + public DynAction (int tag) + { + this.tag = tag; + } + + //~ Methods ------------------------------------------------------------ + /** + * Method to update the action according to the current context + */ + public abstract void update (); + + /** + * Report which item class should be used to the related menu item + * + * @return the precise menu item class + */ + public JMenuItem getMenuItem () + { + return new JMenuItem(this); + } + } + + //--------------// + // AssignAction // + //--------------// + /** + * Assign to each glyph the shape selected in the menu. + */ + protected class AssignAction + extends DynAction + { + //~ Constructors ------------------------------------------------------- + + public AssignAction () + { + super(20); + } + + //~ Methods ------------------------------------------------------------ + @Override + public void actionPerformed (ActionEvent e) + { + // Default action is to open the menu + assert false; + } + + @Override + public JMenuItem getMenuItem () + { + JMenu menu = new JMenu(this); + ShapeSet.addAllShapes(menu, new AssignListener(false)); + + return menu; + } + + @Override + public void update () + { + if ((glyphNb > 0) && noVirtuals) { + setEnabled(true); + + if (glyphNb == 1) { + putValue(NAME, "Assign glyph as ..."); + } else { + putValue(NAME, "Assign each glyph as ..."); + } + + putValue(SHORT_DESCRIPTION, "Manually force an assignment"); + } else { + setEnabled(false); + putValue(NAME, "Assign glyph as ..."); + putValue(SHORT_DESCRIPTION, "No glyph to assign a shape to"); + } + } + } + + //----------------// + // DeassignAction // + //----------------// + /** + * Deassign each glyph in the selected collection of glyphs. + */ + protected class DeassignAction + extends DynAction + { + //~ Constructors ------------------------------------------------------- + + public DeassignAction () + { + super(20); + } + + //~ Methods ------------------------------------------------------------ + @Override + public void actionPerformed (ActionEvent e) + { + // Remember which is the current selected glyph + Glyph glyph = nest.getSelectedGlyph(); + + // Actually deassign the whole set + Set glyphs = nest.getSelectedGlyphSet(); + + if (noVirtuals) { + controller.asyncDeassignGlyphs(glyphs); + + // Update focus on current glyph, if reused in a compound + if (glyph != null) { + Glyph newGlyph = glyph.getFirstSection() + .getGlyph(); + + if (glyph != newGlyph) { + nest.getGlyphService() + .publish( + new GlyphEvent( + this, + SelectionHint.GLYPH_INIT, + null, + newGlyph)); + } + } + } else { + controller.asyncDeleteVirtualGlyphs(glyphs); + } + } + + @Override + public void update () + { + if ((knownNb > 0) && (noVirtuals || (virtualNb == knownNb))) { + setEnabled(true); + + StringBuilder sb = new StringBuilder(); + + if (noVirtuals) { + sb.append("Deassign "); + sb.append(knownNb) + .append(" glyph"); + + if (knownNb > 1) { + sb.append("s"); + } + } else { + sb.append("Delete "); + + if (virtualNb > 0) { + sb.append(virtualNb) + .append(" virtual glyph"); + + if (virtualNb > 1) { + sb.append("s"); + } + } + } + + if (stemNb > 0) { + sb.append(" w/ ") + .append(stemNb) + .append(" stem"); + + if (stemNb > 1) { + sb.append("s"); + } + } + + putValue(NAME, sb.toString()); + putValue(SHORT_DESCRIPTION, "Deassign selected glyphs"); + } else { + setEnabled(false); + putValue(NAME, "Deassign"); + putValue(SHORT_DESCRIPTION, "No glyph to deassign"); + } + } + } + + //-------------// + // PasteAction // + //-------------// + /** + * Paste the latest shape to the glyph(s) at end. + */ + protected class PasteAction + extends DynAction + { + //~ Static fields/initializers ----------------------------------------- + + private static final String PREFIX = "Paste "; + + //~ Constructors ------------------------------------------------------- + public PasteAction () + { + super(10); + } + + //~ Methods ------------------------------------------------------------ + @Override + public void actionPerformed (ActionEvent e) + { + JMenuItem source = (JMenuItem) e.getSource(); + Shape shape = Shape.valueOf( + source.getText().substring(PREFIX.length())); + Glyph glyph = nest.getSelectedGlyph(); + + if (glyph != null) { + controller.asyncAssignGlyphs( + Collections.singleton(glyph), + shape, + false); + } + } + + @Override + public void update () + { + Shape latest = controller.getLatestShapeAssigned(); + + if ((glyphNb > 0) && (latest != null) && noVirtuals) { + setEnabled(true); + putValue(NAME, PREFIX + latest.toString()); + putValue(SHORT_DESCRIPTION, "Assign latest shape"); + } else { + setEnabled(false); + putValue(NAME, PREFIX); + putValue(SHORT_DESCRIPTION, "No shape to assign again"); + } + } + } +} diff --git a/src/main/omr/glyph/ui/GlyphsController.java b/src/main/omr/glyph/ui/GlyphsController.java new file mode 100644 index 0000000..3f16e17 --- /dev/null +++ b/src/main/omr/glyph/ui/GlyphsController.java @@ -0,0 +1,306 @@ +//----------------------------------------------------------------------------// +// // +// G l y p h s C o n t r o l l e r // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.glyph.ui; + +import omr.glyph.Evaluation; +import omr.glyph.Glyphs; +import omr.glyph.GlyphsModel; +import omr.glyph.Nest; +import omr.glyph.Shape; +import omr.glyph.ShapeSet; +import omr.glyph.facets.Glyph; + +import omr.script.AssignTask; +import omr.script.BarlineTask; +import omr.script.DeleteTask; + +import omr.selection.GlyphEvent; +import omr.selection.SelectionHint; +import omr.selection.SelectionService; + +import omr.sheet.Sheet; + +import org.jdesktop.application.Task; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Collection; +import java.util.Set; + +/** + * Class {@code GlyphsController} is a common basis for glyph handling, + * used by any user interface which needs to act on the actual glyph + * data. + * + *

There are two main methods in this class ({@link #asyncAssignGlyphs} and + * {@link #asyncDeassignGlyphs}). They share common characteristics: + *

    + *
  • They are processed asynchronously
  • + *
  • Their action is recorded in the sheet script
  • + *
  • They update the following steps, if any
  • + *
+ * + *

Since the bus of user selections is used, the methods of this class are + * meant to be used from within a user action, otherwise you must use a direct + * access to similar synchronous actions in the underlying {@link GlyphsModel}. + * + * @author Hervé Bitteur + */ +public class GlyphsController +{ + //~ Static fields/initializers --------------------------------------------- + + /** Usual logger utility */ + private static final Logger logger = LoggerFactory.getLogger( + GlyphsController.class); + + //~ Instance fields -------------------------------------------------------- + /** Related model */ + protected final GlyphsModel model; + + /** Cached sheet */ + protected final Sheet sheet; + + //~ Constructors ----------------------------------------------------------- + //------------------// + // GlyphsController // + //------------------// + /** + * Create an instance of GlyphsController, with its underlying + * GlyphsModel instance. + * + * @param model the related glyphs model + */ + public GlyphsController (GlyphsModel model) + { + this.model = model; + + sheet = model.getSheet(); + } + + //~ Methods ---------------------------------------------------------------- + //-------------------// + // asyncAssignGlyphs // + //-------------------// + /** + * Asynchronouly assign a shape to the selected collection of glyphs + * and record this action in the script. + * + * @param glyphs the collection of glyphs to be assigned + * @param shape the shape to be assigned + * @param compound flag to build one compound, rather than assign each + * individual glyph + * @return the task that carries out the processing + */ + public Task asyncAssignGlyphs (Collection glyphs, + Shape shape, + boolean compound) + { + // Safety check: we cannot alter virtual glyphs + for (Glyph glyph : glyphs) { + if (glyph.isVirtual()) { + logger.warn("Cannot alter VirtualGlyph#{}", glyph.getId()); + + return null; + } + } + + if (ShapeSet.Barlines.contains(shape) + || Glyphs.containsBarline(glyphs)) { + // Special case for barlines assignment or deassignment + return new BarlineTask(sheet, shape, compound, glyphs).launch( + sheet); + } else { + // Normal symbol processing + return new AssignTask(sheet, shape, compound, glyphs).launch(sheet); + } + } + + //---------------------// + // asyncDeassignGlyphs // + //---------------------// + /** + * Asynchronously de-Assign a collection of glyphs and record this + * action in the script. + * + * @param glyphs the collection of glyphs to be de-assigned + * @return the task that carries out the processing + */ + public Task asyncDeassignGlyphs (Collection glyphs) + { + return asyncAssignGlyphs(glyphs, null, false); + } + + //--------------------------// + // asyncDeleteVirtualGlyphs // + //--------------------------// + public Task asyncDeleteVirtualGlyphs (Collection glyphs) + { + return new DeleteTask(sheet, glyphs).launch(sheet); + } + + //--------------// + // getGlyphById // + //--------------// + /** + * Retrieve a glyph, knowing its id. + * + * @param id the glyph id + * @return the glyph found, or null if not + */ + public Glyph getGlyphById (int id) + { + return model.getGlyphById(id); + } + + //----------------// + // getLatestShape // + //----------------// + /** + * Report the latest non null shape that was assigned, or null + * if none. + * + * @return latest shape assigned, or null if none + */ + public Shape getLatestShapeAssigned () + { + return model.getLatestShape(); + } + + //--------------------// + // getLocationService // + //--------------------// + /** + * Report the event service to use for LocationEvent. + * When no sheet is available, override this method to point to another + * service + * + * @return the event service to use for LocationEvent + */ + public SelectionService getLocationService () + { + return model.getSheet() + .getLocationService(); + } + + //----------// + // getModel // + //----------// + /** + * Report the underlying model. + * + * @return the underlying glpyhs model + */ + public GlyphsModel getModel () + { + return model; + } + + //---------// + // getNest // + //---------// + /** + * Report the underlying glyph nest. + * + * @return the related glyph nest + */ + public Nest getNest () + { + return model.getNest(); + } + + //----------------// + // setLatestShape // + //----------------// + /** + * Assign the latest shape. + * + * @param shape the latest shape + */ + public void setLatestShapeAssigned (Shape shape) + { + model.setLatestShape(shape); + } + + //------------// + // syncAssign // + //------------// + /** + * Process synchronously the assignment defined in the provided + * context. + * + * @param context the context of the assignment + */ + public void syncAssign (AssignTask context) + { + final boolean compound = context.isCompound(); + final Shape shape = context.getAssignedShape(); + logger.debug("syncAssign {} compound:{}", context, compound); + + Set glyphs = context.getInitialGlyphs(); + + if (shape != null) { // Assignment + // Persistent? + model.assignGlyphs( + glyphs, + context.getAssignedShape(), + compound, + Evaluation.MANUAL); + + // Publish modifications (about new glyph) + Glyph firstGlyph = glyphs.iterator() + .next(); + + if (firstGlyph != null) { + publish(firstGlyph.getMembers().first().getGlyph()); + } + } else { // Deassignment + model.deassignGlyphs(glyphs); + + // Publish modifications (about current glyph) + publish(glyphs.iterator().next()); + } + } + + //------------// + // syncDelete // + //------------// + /** + * Process synchronously the deletion defined in the provided + * context. + * + * @param context the context of the deletion + */ + public void syncDelete (DeleteTask context) + { + logger.debug("syncDelete{}", context); + + model.deleteGlyphs(context.getInitialGlyphs()); + + publish((Glyph) null); + } + + //---------// + // publish // + //---------// + protected void publish (Glyph glyph) + { + // Update immediately the glyph info as displayed + if (model.getSheet() != null) { + getNest() + .getGlyphService() + .publish( + new GlyphEvent(this, SelectionHint.GLYPH_MODIFIED, null, glyph)); + } + } +} diff --git a/src/main/omr/glyph/ui/NestView.java b/src/main/omr/glyph/ui/NestView.java new file mode 100644 index 0000000..8a2f7f1 --- /dev/null +++ b/src/main/omr/glyph/ui/NestView.java @@ -0,0 +1,467 @@ +//----------------------------------------------------------------------------// +// // +// N e s t V i e w // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.glyph.ui; + +import omr.constant.Constant; +import omr.constant.ConstantSet; + +import omr.graph.DigraphView; + +import omr.glyph.Nest; +import omr.glyph.facets.Glyph; + +import omr.lag.Lag; +import omr.lag.Section; + +import omr.score.entity.PartNode; +import omr.score.ui.PaintingParameters; + +import omr.text.FontInfo; +import omr.text.TextChar; +import omr.text.TextLine; +import omr.text.TextWord; + +import omr.ui.Colors; +import omr.ui.util.UIUtil; +import omr.ui.view.RubberPanel; + +import omr.util.WeakPropertyChangeListener; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.awt.Color; +import java.awt.Graphics2D; +import java.awt.Rectangle; +import java.awt.Stroke; +import java.awt.geom.Ellipse2D; +import java.awt.geom.Line2D; +import java.awt.geom.Path2D; +import java.awt.geom.Point2D; +import java.beans.PropertyChangeEvent; +import java.beans.PropertyChangeListener; +import java.lang.ref.WeakReference; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + +/** + * Class {@code NestView} is a view that combines the display of + * several lags to represent a nest of glyphs. + * + * @author Hervé Bitteur + */ +public class NestView + extends RubberPanel + implements DigraphView, PropertyChangeListener +{ + //~ Static fields/initializers --------------------------------------------- + + /** Specific application parameters */ + private static final Constants constants = new Constants(); + + /** Usual logger utility */ + private static final Logger logger = LoggerFactory.getLogger(NestView.class); + + //~ Instance fields -------------------------------------------------------- + /** The underlying nest */ + protected final Nest nest; + + /** Related glyphs controller */ + protected final GlyphsController controller; + + /** The sequence of lags */ + protected final List lags; + + /** Additional items rendering */ + protected final List itemRenderers = new ArrayList<>(); + + //~ Constructors ----------------------------------------------------------- + //----------// + // NestView // + //----------// + /** + * Create a nest view. + * + * @param nest the underlying nest of glyphs + * @param controller the related glyphs controller + * @param lags the various lags to be displayed + */ + public NestView (Nest nest, + GlyphsController controller, + List lags) + { + this.nest = nest; + this.controller = controller; + this.lags = lags; + + setName(nest.getName() + "-View"); + + setBackground(Color.white); + + // (Weakly) listening on ViewParameters and PaintingParameters + PropertyChangeListener listener = new WeakPropertyChangeListener(this); + ViewParameters.getInstance() + .addPropertyChangeListener(listener); + PaintingParameters.getInstance() + .addPropertyChangeListener(listener); + } + + //~ Methods ---------------------------------------------------------------- + //-----------------// + // addItemRenderer // + //-----------------// + /** + * Register an items renderer to renderAttachments items. + * + * @param renderer the additional renderer + */ + public void addItemRenderer (ItemRenderer renderer) + { + itemRenderers.add(new WeakItemRenderer(renderer)); + } + + //---------------// + // getController // + //---------------// + public GlyphsController getController () + { + return controller; + } + + //----------------// + // propertyChange // + //----------------// + @Override + public void propertyChange (PropertyChangeEvent evt) + { + // Whatever the property change, we simply repaint the view + repaint(); + } + + //---------// + // refresh // + //---------// + @Override + public void refresh () + { + repaint(); + } + + //--------// + // render // + //--------// + /** + * Render the nest in the provided Graphics context, which may be + * already scaled. + * + * @param g the graphics context + */ + @Override + public void render (Graphics2D g) + { + // Should we draw the section borders? + final boolean drawBorders = ViewParameters.getInstance().isSectionMode(); + + // Stroke for borders + final Stroke oldStroke = UIUtil.setAbsoluteStroke(g, 1f); + + if (lags != null) { + for (Lag lag : lags) { + // Render all sections, using the colors they have been assigned + for (Section section : lag.getVertices()) { + section.render(g, drawBorders); + } + } + } + + // Paint additional items, such as recognized items, etc... + renderItems(g); + + // Restore stroke + g.setStroke(oldStroke); + } + + //-----------------// + // renderGlyphArea // + //-----------------// + /** + * Render the box area of a glyph, using inverted color. + * + * @param glyph the glyph whose area is to be rendered + * @param g the graphic context + */ + protected void renderGlyphArea (Glyph glyph, + Graphics2D g) + { + // Check the clipping + Rectangle box = glyph.getBounds(); + + if ((box != null) && box.intersects(g.getClipBounds())) { + g.fillRect(box.x, box.y, box.width, box.height); + } + } + + //-------------// + // renderItems // + //-------------// + /** + * Room for rendering additional items, on top of the basic nest + * itself. + * This default implementation paints the selected glyph set, + * or the selected sections set, if any. + * + * @param g the graphic context + */ + protected void renderItems (Graphics2D g) + { + // Additional renderers if any + for (ItemRenderer renderer : itemRenderers) { + renderer.renderItems(g); + } + + // Render the selected glyph(s) if any + Set glyphs = nest.getSelectedGlyphSet(); + + if (glyphs != null) { + // Decorations first + Stroke oldStroke = UIUtil.setAbsoluteStroke(g, 1f); + g.setColor(Color.blue); + + for (Glyph glyph : glyphs) { + // Draw character boxes for textual glyphs? + if (glyph.isText()) { + if (ViewParameters.getInstance() + .isLetterBoxPainting()) { + TextWord word = glyph.getTextWord(); + + if (word != null) { + for (TextChar ch : word.getChars()) { + Rectangle b = ch.getBounds(); + g.drawRect(b.x, b.y, b.width, b.height); + } + } + } + } + + // Draw attachments, if any + glyph.renderAttachments(g); + + // Draw glyph line? + if (ViewParameters.getInstance().isLinePainting()) { + glyph.renderLine(g); + } + } + + g.setStroke(oldStroke); + } + + // Glyph areas second, using XOR mode for the area + if (!ViewParameters.getInstance().isSectionMode()) { + // Glyph selection mode + if (glyphs != null) { + Graphics2D g2 = (Graphics2D) g.create(); + g2.setColor(Color.black); + g2.setXORMode(Color.darkGray); + + for (Glyph glyph : glyphs) { + renderGlyphArea(glyph, g2); + } + + g2.dispose(); + + // Display words of a sentence, if any + if (ViewParameters.getInstance().isSentencePainting()) { + for (Glyph glyph : glyphs) { + renderGlyphSentence(glyph, g); + } + } + + // Display translation links, if any + if (ViewParameters.getInstance().isTranslationPainting()) { + for (Glyph glyph : glyphs) { + renderGlyphTranslations(glyph, g); + } + } + } + } else { + // Section selection mode + for (Lag lag : lags) { + Set

selected = lag.getSelectedSectionSet(); + + if ((selected != null) && !selected.isEmpty()) { + for (Section section : selected) { + section.renderSelected(g); + } + } + } + } + } + + //-------------------------// + // renderGlyphTranslations // + //-------------------------// + private void renderGlyphTranslations (Glyph glyph, + Graphics2D g) + { + if (glyph.getTranslations().isEmpty()) { + return; + } + + Stroke oldStroke = UIUtil.setAbsoluteStroke(g, 1f); + Color oldColor = g.getColor(); + g.setColor(Colors.TRANSLATION_LINK); + + // Compute end radius, with fixed size whatever the current zoom + double r = 1 / g.getTransform().getScaleX(); + + for (PartNode node : glyph.getTranslations()) { + for (Line2D line : node.getTranslationLinks(glyph)) { + // Draw line + g.draw(line); + + // Draw ending points + Ellipse2D e1 = new Ellipse2D.Double( + line.getX1() - r, line.getY1() - r, 2 * r, 2 * r); + g.draw(e1); + Ellipse2D e2 = new Ellipse2D.Double( + line.getX2() - r, line.getY2() - r, 2 * r, 2 * r); + g.draw(e2); + } + } + + g.setColor(oldColor); + g.setStroke(oldStroke); + } + + //---------------------// + // renderGlyphSentence // + //---------------------// + /** + * Display the relation between the glyph/word at hand and the other + * words of the same containing sentence + * + * @param glyph the provided selected glyph + * @param g graphic context + */ + private void renderGlyphSentence (Glyph glyph, + Graphics2D g) + { + if (glyph.getTextWord() == null) { + return; + } + + TextLine sentence = glyph.getTextWord().getTextLine(); + Color oldColor = g.getColor(); + + if (constants.showSentenceBaseline.isSet()) { + // Display the whole sentence baseline + g.setColor(Colors.SENTENCE_BASELINE); + Stroke oldStroke = UIUtil.setAbsoluteStroke(g, 1f); + + Path2D path = new Path2D.Double(); + TextWord prevWord = null; + for (TextWord word : sentence.getWords()) { + Point2D left = word.getBaseline().getP1(); + if (prevWord == null) { + path.moveTo(left.getX(), left.getY()); + } else { + path.lineTo(left.getX(), left.getY()); + } + + Point2D right = word.getBaseline().getP2(); + path.lineTo(right.getX(), right.getY()); + prevWord = word; + } + g.draw(path); + + g.setStroke(oldStroke); + } else { + // Display a x-height rectangle between words + g.setColor(Colors.SENTENCE_GAPS); + FontInfo font = sentence.getMeanFont(); + double height = font.pointsize * 0.4f; // TODO: Explain this 0.4 + + TextWord prevWord = null; + for (TextWord word : sentence.getWords()) { + if (prevWord != null) { + Path2D path = new Path2D.Double(); + Point2D from = prevWord.getBaseline().getP2(); + path.moveTo(from.getX(), from.getY()); + path.lineTo(from.getX(), from.getY() - height); + Point2D to = word.getBaseline().getP1(); + path.lineTo(to.getX(), to.getY() - height); + path.lineTo(to.getX(), to.getY()); + path.closePath(); + + g.fill(path); + } + prevWord = word; + } + } + + g.setColor(oldColor); + } + + //~ Inner Interfaces ------------------------------------------------------- + //--------------// + // ItemRenderer // + //--------------// + /** + * Used to plug additional items renderers to this view. + */ + public static interface ItemRenderer + { + //~ Methods ------------------------------------------------------------ + + void renderItems (Graphics2D g); + } + + //------------------// + // WeakItemRenderer // + //------------------// + private static class WeakItemRenderer + implements ItemRenderer + { + + protected final WeakReference weakRenderer; + + public WeakItemRenderer (ItemRenderer renderer) + { + weakRenderer = new WeakReference<>(renderer); + } + + @Override + public void renderItems (Graphics2D g) + { + ItemRenderer renderer = weakRenderer.get(); + + if (renderer != null) { + renderer.renderItems(g); + } + } + } + + //-----------// + // Constants // + //-----------// + private static final class Constants + extends ConstantSet + { + //~ Instance fields ---------------------------------------------------- + + Constant.Boolean showSentenceBaseline = new Constant.Boolean( + true, + "Should we show sentence baseline (vs inter-word gaps)?"); + + } +} diff --git a/src/main/omr/glyph/ui/SampleVerifier.java b/src/main/omr/glyph/ui/SampleVerifier.java new file mode 100644 index 0000000..e0f42bd --- /dev/null +++ b/src/main/omr/glyph/ui/SampleVerifier.java @@ -0,0 +1,740 @@ +//----------------------------------------------------------------------------// +// // +// S a m p l e V e r i f i e r // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.glyph.ui; + +import omr.WellKnowns; + +import omr.glyph.GlyphRepository; +import omr.glyph.Shape; + +import omr.ui.MainGui; + +import com.jgoodies.forms.builder.PanelBuilder; +import com.jgoodies.forms.layout.CellConstraints; +import com.jgoodies.forms.layout.FormLayout; + +import org.jdesktop.application.ResourceMap; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.awt.BorderLayout; +import java.awt.Component; +import java.awt.Dimension; +import java.awt.GridLayout; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.io.File; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.EnumSet; +import java.util.List; +import java.util.SortedSet; +import java.util.TreeSet; + +import javax.swing.BorderFactory; +import javax.swing.DefaultListModel; +import javax.swing.JButton; +import javax.swing.JFrame; +import javax.swing.JLabel; +import javax.swing.JList; +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.JSplitPane; +import javax.swing.ListCellRenderer; +import javax.swing.border.EtchedBorder; +import javax.swing.border.TitledBorder; +import javax.swing.event.ChangeEvent; +import javax.swing.event.ChangeListener; +import javax.swing.event.ListSelectionEvent; +import javax.swing.event.ListSelectionListener; + +/** + * Class {@code SampleVerifier} provides a user interface to browse + * through all glyphs samples recorded for evaluator training, + * to visually check the correctness of their assigned shape, + * and to remove spurious samples when necessary. + * + *

One, several or all recorded sheets can be selected. + * + *

Within the contained glyphs, one, several or all can be selected, the + * selected glyphs can then be browsed in any direction. + * + *

The current glyph is displayed, with its appearance in a properly + * translated Nest view, and its characteristics in a dedicated panel. + * If the user wants to discard the glyph, it can be removed from the repository + * of training material. + * + * @author Hervé Bitteur + */ +public class SampleVerifier +{ + //~ Static fields/initializers --------------------------------------------- + + /** Usual logger utility */ + private static final Logger logger = LoggerFactory.getLogger(SampleVerifier.class); + + /** The unique instance */ + private static volatile SampleVerifier INSTANCE; + + //~ Instance fields -------------------------------------------------------- + /** Repository of known glyphs */ + private final GlyphRepository repository = GlyphRepository.getInstance(); + + /** The dedicated frame */ + private final JFrame frame; + + /** The panel in charge of the current glyph */ + private GlyphBrowser glyphBrowser = new GlyphBrowser(this); + + /** The panel in charge of the glyphs selection */ + private GlyphSelector glyphSelector = new GlyphSelector(glyphBrowser); + + /** The panel in charge of the shapes selection */ + private ShapeSelector shapeSelector = new ShapeSelector(glyphSelector); + + /** The panel in charge of the sheets (or icons folder) selection */ + private FolderSelector folderSelector = new FolderSelector(shapeSelector); + + /** Sheets folder */ + private final File sheetsFolder = repository.getSheetsFolder(); + + /** Samples folder */ + private final File samplesFolder = repository.getSamplesFolder(); + + //~ Constructors ----------------------------------------------------------- + //----------------// + // SampleVerifier // + //----------------// + /** + * Create an instance of SampleVerifier. + */ + private SampleVerifier () + { + // Pane split vertically: selectors then browser + JSplitPane vertSplitPane = new JSplitPane( + JSplitPane.VERTICAL_SPLIT, + getSelectorsPanel(), + glyphBrowser.getComponent()); + vertSplitPane.setName("SampleVerifierSplitPane"); + vertSplitPane.setDividerSize(1); + + // Hosting frame + frame = new JFrame(); + frame.setName("SampleVerifierFrame"); + frame.add(vertSplitPane); + + // Resource injection + ResourceMap resource = MainGui.getInstance().getContext(). + getResourceMap( + getClass()); + resource.injectComponents(frame); + } + + //~ Methods ---------------------------------------------------------------- + //-------------// + // getInstance // + //-------------// + /** + * Give access to the single instance of this class. + * + * @return the SampleVerifier instance + */ + public static SampleVerifier getInstance () + { + if (INSTANCE == null) { + INSTANCE = new SampleVerifier(); + } + + return INSTANCE; + } + + //------------// + // setVisible // + //------------// + /** + * Make the UI frame visible or not. + * + * @param bool true for visible, false for hidden + */ + public void setVisible (boolean bool) + { + MainGui.getInstance().show(frame); + } + + //--------// + // verify // + //--------// + /** + * Focus the verifier on a provided collection of glyphs. + * (typically this collection are the glyphs that are not recognized, + * or mistaken, by the evaluator) + * + * @param glyphNames the names of the specific glyphs to inspect + */ + public void verify (Collection glyphNames) + { + // Glyphs + glyphSelector.populateWith(glyphNames); + glyphSelector.selectAll(); + + // Shapes + EnumSet shapeSet = EnumSet.noneOf(Shape.class); + + for (String gName : glyphNames) { + File file = new File(gName); + shapeSet.add(Shape.valueOf(radixOf(file.getName()))); + } + + shapeSelector.populateWith(shapeSet); + shapeSelector.selectAll(); + + // Sheets / Icons folder + SortedSet folderSet = new TreeSet<>(); + + for (String gName : glyphNames) { + File file = new File(gName); + folderSet.add(file.getParent()); + } + + folderSelector.populateWith(folderSet); + folderSelector.selectAll(); + + // Load the first glyph in the browser + glyphBrowser.loadGlyphNames(); + } + + //-----------------// + // deleteGlyphName // + //-----------------// + /** + * Remove a glyph name from the current selection. + * + * @param gName the glyph name to remove + */ + void deleteGlyphName (String gName) + { + // Remove entry from glyph list + glyphSelector.model.removeElement(gName); + } + + //---------------// + // getGlyphCount // + //---------------// + /** + * Report the number of currently selected glyphs names. + * + * @return the number of selected glyphs names + */ + int getGlyphCount () + { + return glyphSelector.list.getSelectedIndices().length; + } + + //---------------// + // getGlyphNames // + //---------------// + /** + * Report the collection of currently selected glyphs names. + * + * @return an list of glyphs names + */ + List getGlyphNames () + { + return glyphSelector.list.getSelectedValuesList(); + } + + //---------// + // radixOf // + //---------// + private static String radixOf (String path) + { + int i = path.indexOf('.'); + + if (i >= 0) { + return path.substring(0, i); + } else { + return ""; + } + } + + //--------------// + // getActualDir // + //--------------// + /** + * Report the real directory that corresponds to a given folder name. + * (either the sheets or samples directory or the symbols directory) + * + * @param folder the folder name, such as 'symbols' or 'sheets/batuque' or + * 'samples/batuque' + * @return the concrete directory + */ + private File getActualDir (String folder) + { + if (repository.isIconsFolder(folder)) { + return WellKnowns.SYMBOLS_FOLDER; + } else { + int slashPos = folder.indexOf(File.separatorChar); + String root = folder.substring(0, slashPos); + String name = folder.substring(slashPos + 1); + + if (root.equals(sheetsFolder.getName())) { + return new File(sheetsFolder, name); + } else if (root.equals(samplesFolder.getName())) { + return new File(samplesFolder, name); + } else { + throw new IllegalArgumentException("Unexpected root: " + root); + } + } + } + + //-------------------// + // getSelectorsPanel // + //-------------------// + private JPanel getSelectorsPanel () + { + FormLayout layout = new FormLayout( + "max(100dlu;pref),max(150dlu;pref),max(200dlu;pref):grow", // Cols + "pref:grow"); // Rows + + PanelBuilder builder = new PanelBuilder(layout); + builder.setDefaultDialogBorder(); + + CellConstraints cst = new CellConstraints(); + + int r = 1; // -------------------------------- + builder.add(folderSelector, cst.xy(1, r)); + builder.add(shapeSelector, cst.xy(2, r)); + builder.add(glyphSelector, cst.xy(3, r)); + + return builder.getPanel(); + } + + //~ Inner Classes ---------------------------------------------------------- + //----------------// + // FolderSelector // + //----------------// + private class FolderSelector + extends Selector + { + //~ Constructors ------------------------------------------------------- + + public FolderSelector (ChangeListener listener) + { + super("Folders", listener, 300); + load.setEnabled(true); + } + + //~ Methods ------------------------------------------------------------ + // Triggered by load button + @Override + public void actionPerformed (ActionEvent e) + { + model.removeAllElements(); + + // First insert the dedicated icons folder + model.addElement(WellKnowns.SYMBOLS_FOLDER.getName()); + + // Then the sheets folders + String root = repository.getSheetsFolder().getName(); + ArrayList folders = new ArrayList<>(); + + for (File file : repository.getSheetDirectories()) { + folders.add(root + File.separator + file.getName()); + } + + // Finally, the samples folders + root = repository.getSamplesFolder().getName(); + + for (File file : repository.getSampleDirectories()) { + folders.add(root + File.separator + file.getName()); + } + + Collections.sort(folders); + + for (String folder : folders) { + model.addElement(folder); + } + + updateCardinal(); + } + } + + //---------------// + // GlyphSelector // + //---------------// + private class GlyphSelector + extends Selector + { + //~ Constructors ------------------------------------------------------- + + public GlyphSelector (ChangeListener listener) + { + super("Glyphs", listener, 300); + } + + //~ Methods ------------------------------------------------------------ + // Triggered by the load button + @Override + public void actionPerformed (ActionEvent e) + { + final List folders = folderSelector.list. + getSelectedValuesList(); + final List shapes = shapeSelector.list. + getSelectedValuesList(); + + // Debug + if (logger.isDebugEnabled()) { + logger.debug("Glyph Selector. Got Folders:"); + + for (String fName : folders) { + logger.debug(fName); + } + + logger.debug("Glyph Selector. Got Shapes:"); + + for (Shape shape : shapes) { + logger.debug(shape.toString()); + } + } + + if (shapes.isEmpty()) { + logger.warn("No shapes selected in Shape Selector"); + } else { + model.removeAllElements(); + + // Populate with all possible glyphs, sorted by gName + for (String folder : folders) { + // Add proper glyphs files from this directory + ArrayList gNames = new ArrayList<>(); + File dir = getActualDir(folder); + + for (File file : repository.getGlyphsIn(dir)) { + String shapeName = radixOf(file.getName()); + Shape shape = Shape.valueOf(shapeName); + + if (shapes.contains(shape)) { + gNames.add( + folder + File.separator + file.getName()); + } + } + + Collections.sort(gNames); + + for (String gName : gNames) { + model.addElement(gName); + } + } + + updateCardinal(); + } + } + } + + //----------// + // Selector // + //----------// + /** + * Class {@code Selector} defines the common properties of sheet, + * shape and glyph selectors. + * Each selector is made of a list of names, which can be selected and + * deselected at will. + */ + private abstract static class Selector + extends TitledPanel + implements ActionListener, + ChangeListener + { + //~ Instance fields ---------------------------------------------------- + + /** The title base for this selector */ + private final String title; + + /** Other entity interested in items selected by this selector */ + private ChangeListener listener; + + /** Change event, lazily created */ + private ChangeEvent changeEvent; + + // Buttons + protected JButton load = new JButton("Load"); + + protected JButton selectAll = new JButton( + "Select All"); + + protected JButton cancelAll = new JButton( + "Cancel All"); + + // List of items, with its model + protected final DefaultListModel model = new DefaultListModel<>(); + + protected JList list = new JList<>(model); + + // ScrollPane around the list + protected JScrollPane scrollPane = new JScrollPane(list); + + //~ Constructors ------------------------------------------------------- + //----------// + // Selector // + //----------// + /** + * Create a selector. + * + * @param title label for this selector + * @param listener potential (external) listener for changes + * @param preferred width + */ + public Selector (String title, + ChangeListener listener, + int width) + { + super(title, width); + this.title = title; + this.listener = listener; + + // Precise action to be specified in each subclass + load.addActionListener(this); + + ///list.setVisibleRowCount(10); + ///scrollPane.setMinimumSize(new Dimension(250, 300)); + + // To be informed of mouse (de)selections (not programmatic) + list.addListSelectionListener( + new ListSelectionListener() + { + @Override + public void valueChanged (ListSelectionEvent e) + { + updateCardinal(); // Brute force !!! + } + }); + + // Same action whatever the subclass : select all items + selectAll.addActionListener( + new ActionListener() + { + @Override + public void actionPerformed (ActionEvent e) + { + selectAll(); + } + }); + + // Same action whatever the subclass : deselect all items + cancelAll.addActionListener( + new ActionListener() + { + @Override + public void actionPerformed (ActionEvent e) + { + list.setSelectedIndices(new int[0]); + updateCardinal(); + } + }); + + JPanel buttons = new JPanel(new GridLayout(3, 1)); + buttons.add(load); + buttons.add(selectAll); + buttons.add(cancelAll); + + // All buttons are initially disabled + load.setEnabled(false); + selectAll.setEnabled(false); + cancelAll.setEnabled(false); + + add(buttons, BorderLayout.WEST); + add(scrollPane, BorderLayout.CENTER); + } + + //~ Methods ------------------------------------------------------------ + //--------------// + // populateWith // + //--------------// + public void populateWith (Collection items) + { + model.removeAllElements(); + + for (E item : items) { + model.addElement(item); + } + + updateCardinal(); + } + + //-----------// + // selectAll // + //-----------// + public void selectAll () + { + list.setSelectionInterval(0, model.size() - 1); + updateCardinal(); + } + + //--------------// + // stateChanged // + //--------------// + @Override + public void stateChanged (ChangeEvent e) + { + Selector selector = (Selector) e.getSource(); + int selNb = selector.list.getSelectedIndices().length; + load.setEnabled(selNb > 0); + } + + //----------------// + // updateCardinal // + //----------------// + protected void updateCardinal () + { + int[] selection = list.getSelectedIndices(); + int selectNb = selection.length; + + TitledBorder border = (TitledBorder) getBorder(); + + if (selectNb > 0) { + border.setTitle(title + ": " + selectNb); + } else { + border.setTitle(title); + } + + // Buttons + selectAll.setEnabled(model.size() > 0); + cancelAll.setEnabled(selection.length > 0); + + // Notify other entity + if (listener != null) { + if (changeEvent == null) { + changeEvent = new ChangeEvent(this); + } + + listener.stateChanged(changeEvent); + } + + repaint(); + } + } + + //-------------------// + // ShapeCellRenderer // + //-------------------// + private class ShapeCellRenderer + extends JLabel + implements ListCellRenderer + { + //~ Constructors ------------------------------------------------------- + + public ShapeCellRenderer () + { + setOpaque(true); + } + + //~ Methods ------------------------------------------------------------ + + /* + * This method finds the image and text corresponding + * to the selected value and returns the label, set up + * to display the text and image. + */ + @Override + public Component getListCellRendererComponent ( + JList list, + Shape shape, + int index, + boolean isSelected, + boolean cellHasFocus) + { + if (isSelected) { + setBackground(list.getSelectionBackground()); + setForeground(list.getSelectionForeground()); + } else { + setBackground(list.getBackground()); + setForeground(shape.getColor()); + } + + setFont(list.getFont()); + setText(shape.toString()); + setIcon(shape.getDecoratedSymbol()); + + return this; + } + } + + //---------------// + // ShapeSelector // + //---------------// + private class ShapeSelector + extends Selector + { + //~ Constructors ------------------------------------------------------- + + public ShapeSelector (ChangeListener listener) + { + super("Shapes", listener, 150); + list.setCellRenderer(new ShapeCellRenderer()); + + ///list.setFixedCellHeight(60); + } + + //~ Methods ------------------------------------------------------------ + // Triggered by load button + @Override + public void actionPerformed (ActionEvent e) + { + // Populate with shape names found in selected folders + List folders = folderSelector.list.getSelectedValuesList(); + + if (folders.isEmpty()) { + logger.warn("No folders selected in Folder Selector"); + } else { + EnumSet shapeSet = EnumSet.noneOf(Shape.class); + + for (String folder : folders) { + File dir = getActualDir(folder); + + // Add all glyphs files from this directory + for (File file : repository.getGlyphsIn(dir)) { + shapeSet.add(Shape.valueOf(radixOf(file.getName()))); + } + } + + populateWith(shapeSet); + } + } + } + + //-------------// + // TitledPanel // + //-------------// + private static class TitledPanel + extends JPanel + { + //~ Instance fields ---------------------------------------------------- + + protected final int height = 200; + + //~ Constructors ------------------------------------------------------- + public TitledPanel (String title, + int width) + { + setBorder( + BorderFactory.createTitledBorder( + new EtchedBorder(), + title, + TitledBorder.LEFT, + TitledBorder.TOP)); + setLayout(new BorderLayout()); + setMinimumSize(new Dimension(200, height)); + setPreferredSize(new Dimension(width, height)); + } + } +} diff --git a/src/main/omr/glyph/ui/ShapeBoard.java b/src/main/omr/glyph/ui/ShapeBoard.java new file mode 100644 index 0000000..7bbdfbe --- /dev/null +++ b/src/main/omr/glyph/ui/ShapeBoard.java @@ -0,0 +1,587 @@ +//----------------------------------------------------------------------------// +// // +// S h a p e B o a r d // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.glyph.ui; + +import omr.Main; + +import omr.constant.Constant; +import omr.constant.ConstantSet; + +import omr.glyph.Glyphs; +import omr.glyph.Shape; +import omr.glyph.ShapeSet; +import omr.glyph.facets.Glyph; + +import omr.script.InsertTask; + +import omr.selection.MouseMovement; +import omr.selection.UserEvent; + +import omr.sheet.Sheet; + +import omr.ui.Board; +import omr.ui.dnd.AbstractGhostDropListener; +import omr.ui.dnd.GhostDropAdapter; +import omr.ui.dnd.GhostDropEvent; +import omr.ui.dnd.GhostDropListener; +import omr.ui.dnd.GhostGlassPane; +import omr.ui.dnd.GhostMotionAdapter; +import omr.ui.dnd.ScreenPoint; +import omr.ui.symbol.MusicFont; +import omr.ui.symbol.ShapeSymbol; +import omr.ui.util.Panel; +import omr.ui.view.RubberPanel; +import omr.ui.view.ScrollView; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.awt.Component; +import java.awt.Dimension; +import java.awt.FlowLayout; +import java.awt.Point; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import java.awt.event.MouseListener; +import java.awt.image.BufferedImage; +import java.lang.ref.WeakReference; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import javax.swing.JButton; +import javax.swing.JFrame; + +/** + * Class {@code ShapeBoard} hosts a palette of shapes for insertion and + * assignment of glyph. + *

    + *
  • Direct insertion is performed by drag and drop to the target score + * view or sheet view
  • + *
  • Assignment of existing glyph is performed by a double-click
  • + *
+ * + * @author Hervé Bitteur + */ +public class ShapeBoard + extends Board +{ + //~ Static fields/initializers --------------------------------------------- + + /** Specific application parameters */ + private static final Constants constants = new Constants(); + + /** Usual logger utility */ + private static final Logger logger = LoggerFactory.getLogger(ShapeBoard.class); + + /** To force the width of the various panels */ + private static final int BOARD_WIDTH = 280; + + /** + * To force the height of the various shape panels (just a dirty hack) + */ + private static final Map heights = new HashMap<>(); + + static { + heights.put(ShapeSet.Accidentals, 40); + heights.put(ShapeSet.Articulations, 60); + heights.put(ShapeSet.Attributes, 40); + heights.put(ShapeSet.Barlines, 100); + heights.put(ShapeSet.Beams, 60); + heights.put(ShapeSet.Clefs, 140); + heights.put(ShapeSet.Dynamics, 220); + heights.put(ShapeSet.Flags, 130); + heights.put(ShapeSet.Keys, 220); + heights.put(ShapeSet.NoteHeads, 40); + heights.put(ShapeSet.Markers, 120); + heights.put(ShapeSet.Notes, 40); + heights.put(ShapeSet.Ornaments, 80); + heights.put(ShapeSet.Rests, 120); + heights.put(ShapeSet.Times, 130); + heights.put(ShapeSet.Physicals, 150); + } + + //~ Instance fields -------------------------------------------------------- + /** Related sheet */ + private final Sheet sheet; + + /** The controller in charge of symbol assignments */ + private final SymbolsController symbolsController; + + /** + * Method called when a range is selected: the panel of ranges is + * replaced by the panel of shapes that compose the selected range. + */ + private ActionListener rangeListener = new ActionListener() + { + @Override + public void actionPerformed (ActionEvent e) + { + // Remove panel of ranges + getBody() + .remove(rangesPanel); + + // Replace by proper panel of range shapes + String rangeName = ((JButton) e.getSource()).getName(); + ShapeSet range = ShapeSet.getShapeSet(rangeName); + shapesPanel = shapesPanels.get(range); + + if (shapesPanel == null) { + // Lazily populate the map of shapesPanel instances + shapesPanels.put(range, shapesPanel = defineShapesPanel(range)); + } + + getBody() + .add(shapesPanel); + + // Perhaps this is too much ... TODO + JFrame frame = Main.getGui() + .getFrame(); + frame.invalidate(); + frame.validate(); + frame.repaint(); + } + }; + + /** + * Method called when a panel of shapes is closed: the panel is + * replaced by the panel of ranges to allow the selection of another + * range. + */ + private ActionListener closeListener = new ActionListener() + { + @Override + public void actionPerformed (ActionEvent e) + { + // Remove current panel of shapes + getBody() + .remove(shapesPanel); + + // Replace by panel of ranges + getBody() + .add(rangesPanel); + + // Perhaps this is too much ... TODO + JFrame frame = Main.getGui() + .getFrame(); + frame.invalidate(); + frame.validate(); + frame.repaint(); + } + }; + + /** + * Method called when a shape button is clicked. + */ + private MouseListener mouseListener = new MouseAdapter() + { + // Ability to use the button for direct assignment via double-click + @Override + public void mouseClicked (MouseEvent e) + { + if (e.getClickCount() == 2) { + Glyph glyph = sheet.getNest() + .getSelectedGlyph(); + + if (glyph != null) { + ShapeButton button = (ShapeButton) e.getSource(); + + // Actually assign the shape + symbolsController.asyncAssignGlyphs( + Glyphs.sortedSet(glyph), + button.shape, + false); + } + } + } + }; + + /** Panel of all ranges */ + private final Panel rangesPanel; + + /** Map of shape panels */ + private final Map shapesPanels = new HashMap<>(); + + /** Current panel of shapes */ + private Panel shapesPanel; + + /** GlassPane */ + private GhostGlassPane glassPane = Main.getGui() + .getGlassPane(); + + // Update image and forward mouse location + private final MyMotionAdapter motionAdapter = new MyMotionAdapter( + glassPane); + + // When symbol is dropped + private final GhostDropListener dropListener = new MyDropListener(); + + // When mouse pressed (start) and released (stop) + private final GhostDropAdapter dropAdapter = new MyDropAdapter( + glassPane); + + //~ Constructors ----------------------------------------------------------- + //------------// + // ShapeBoard // + //------------// + /** + * Create a new ShapeBoard object. + * + * @param sheet the related sheet + * @param symbolsController the UI controller for symbols + * @param expanded true if initially expanded + */ + public ShapeBoard (Sheet sheet, + SymbolsController symbolsController, + boolean expanded) + { + super(Board.SHAPE, null, null, false, expanded); + this.symbolsController = symbolsController; + this.sheet = sheet; + + dropAdapter.addDropListener(dropListener); + + getBody() + .add(rangesPanel = defineRangesPanel()); + } + + //~ Methods ---------------------------------------------------------------- + //---------// + // onEvent // + //---------// + /** + * Unused in this board. + * + * @param event unused + */ + @Override + public void onEvent (UserEvent event) + { + // Empty + } + + //-------------------// + // defineRangesPanel // + //-------------------// + /** + * Define the global panel of ranges. + * + * @return the global panel of ranges + */ + private Panel defineRangesPanel () + { + Panel panel = new Panel(); + panel.setNoInsets(); + panel.setPreferredSize(new Dimension(BOARD_WIDTH, 180)); + + FlowLayout layout = new FlowLayout(); + layout.setAlignment(FlowLayout.LEADING); + panel.setLayout(layout); + + for (ShapeSet range : ShapeSet.getShapeSets()) { + Shape rep = range.getRep(); + + if (rep != null) { + JButton button = new JButton(); + button.setIcon(rep.getDecoratedSymbol()); + button.setName(range.getName()); + button.addActionListener(rangeListener); + button.setToolTipText(range.getName()); + button.setBorderPainted(false); + panel.add(button); + } + } + + return panel; + } + + //-------------------// + // defineShapesPanel // + //-------------------// + /** + * Define the panel of shapes for a given range. + * + * @param range the given range of shapes + * @return the panel of shapes for the provided range + */ + private Panel defineShapesPanel (ShapeSet range) + { + Panel panel = new Panel(); + panel.setNoInsets(); + panel.setPreferredSize(new Dimension(BOARD_WIDTH, heights.get(range))); + + FlowLayout layout = new FlowLayout(); + layout.setAlignment(FlowLayout.LEADING); + panel.setLayout(layout); + + // Button to close this shapes panel and return to ranges panel + JButton close = new JButton(range.getName()); + close.addActionListener(closeListener); + close.setToolTipText("Back to ranges"); + close.setBorderPainted(false); + panel.add(close); + + // One button per shape + for (Shape shape : range.getSortedShapes()) { + ShapeButton button = new ShapeButton(shape); + button.addMouseListener(dropAdapter); // For DnD transfer + button.addMouseListener(mouseListener); // For double-click + button.addMouseMotionListener(motionAdapter); // For dragging + panel.add(button); + } + + return panel; + } + + //--------------// + // getIconImage // + //--------------// + /** + * Get the image to draw as an icon for the provided shape. + * + * @param shape the provided shape + * @return an image properly sized for an icon + */ + private BufferedImage getIconImage (Shape shape) + { + ShapeSymbol symbol = (shape == Shape.BEAM_HOOK) + ? shape.getPhysicalShape() + .getSymbol() : shape.getDecoratedSymbol(); + + return symbol.getIconImage(); + } + + //~ Inner Classes ---------------------------------------------------------- + //-----------// + // Constants // + //-----------// + private static final class Constants + extends ConstantSet + { + //~ Instance fields ---------------------------------------------------- + + Constant.Boolean publishLocationWhileDragging = new Constant.Boolean( + false, + "Should we publish the current location while dragging a shape?"); + + } + + //-------------// + // ShapeButton // + //-------------// + /** + * A button dedicated to a shape. + */ + private static class ShapeButton + extends JButton + { + //~ Instance fields ---------------------------------------------------- + + final Shape shape; + + //~ Constructors ------------------------------------------------------- + public ShapeButton (Shape shape) + { + this.shape = shape; + setIcon(shape.getDecoratedSymbol()); + setName(shape.toString()); + setToolTipText(shape.toString()); + + setBorderPainted(true); + } + } + + //---------------// + // MyDropAdapter // + //---------------// + /** + * DnD adapter called when mouse is pressed and released. + */ + private class MyDropAdapter + extends GhostDropAdapter + { + //~ Constructors ------------------------------------------------------- + + public MyDropAdapter (GhostGlassPane glassPane) + { + super(glassPane, null); + } + + //~ Methods ------------------------------------------------------------ + /** Start of DnD, set pay load */ + @Override + public void mousePressed (MouseEvent e) + { + // Reset the motion adapter + motionAdapter.reset(); + + ShapeButton button = (ShapeButton) e.getSource(); + Shape shape = button.shape; + + // Set shape & image + if (shape.isDraggable()) { + action = shape; + image = getIconImage(shape); + } else { + action = Shape.NON_DRAGGABLE; + image = Shape.NON_DRAGGABLE.getSymbol() + .getIconImage(); + } + + super.mousePressed(e); + } + } + + //----------------// + // MyDropListener // + //----------------// + /** + * Listener called when DnD shape is dropped. + */ + private class MyDropListener + extends AbstractGhostDropListener + { + //~ Constructors ------------------------------------------------------- + + public MyDropListener () + { + // Target will be any view of sheet assembly + super(null); + } + + //~ Methods ------------------------------------------------------------ + @Override + public void dropped (GhostDropEvent e) + { + Shape shape = e.getAction(); + + if (shape != Shape.NON_DRAGGABLE) { + ScreenPoint screenPoint = e.getDropLocation(); + + // The (zoomed) sheet view + ScrollView scrollView = sheet.getAssembly() + .getSelectedView(); + + if (screenPoint.isInComponent( + scrollView.getComponent().getViewport())) { + RubberPanel view = scrollView.getView(); + Point localPt = screenPoint.getLocalPoint(view); + view.getZoom() + .unscale(localPt); + + // Asynchronously insert the desired shape at proper location + new InsertTask( + sheet, + shape, + Collections.singleton( + new Point(localPt.x, localPt.y))).launch( + sheet); + } + } + } + } + + //-----------------// + // MyMotionAdapter // + //-----------------// + /** + * Adapter in charge of forwarding the current mouse location and + * updating the dragged image according to the target under the mouse. + */ + private class MyMotionAdapter + extends GhostMotionAdapter + { + //~ Instance fields ---------------------------------------------------- + + // Optimization: remember the latest component on target + private WeakReference prevComponent; + + //~ Constructors ------------------------------------------------------- + public MyMotionAdapter (GhostGlassPane glassPane) + { + super(glassPane); + reset(); + } + + //~ Methods ------------------------------------------------------------ + /** + * In this specific implementation, we update the size of the + * shape image according to the interline scale and to the + * display zoom of the droppable target underneath. + * + * @param e the mouse event + */ + @Override + public void mouseDragged (MouseEvent e) + { + ShapeButton button = (ShapeButton) e.getSource(); + Shape shape = button.shape; + Point absPt = e.getLocationOnScreen(); + ScreenPoint screenPoint = new ScreenPoint(absPt.x, absPt.y); + + // The (zoomed) sheet view + ScrollView scrollView = sheet.getAssembly() + .getSelectedView(); + Component component = scrollView.getComponent() + .getViewport(); + + if (screenPoint.isInComponent(component)) { + RubberPanel view = scrollView.getView(); + + // Publish the current location? + if (constants.publishLocationWhileDragging.getValue()) { + Point localPt = screenPoint.getLocalPoint(view); + view.getZoom() + .unscale(localPt); + view.pointSelected(localPt, MouseMovement.DRAGGING); + } + + // Moving into this component? + if (component != prevComponent.get()) { + glassPane.setOverTarget(true); + + // Try to use full image size, adapted to current zoom + int zoomedInterline = (int) Math.rint( + view.getZoom().getRatio() * sheet.getScale().getInterline()); + Shape displayedShape = shape.isDraggable() ? shape + : Shape.NON_DRAGGABLE; + BufferedImage image = MusicFont.buildImage( + displayedShape, + zoomedInterline, + true); // Decorated + + if (image != null) { + // Use of perfectly sized font-based image + glassPane.setImage(image); + } + + prevComponent = new WeakReference<>(component); + } + } else if (prevComponent.get() != null) { + // No longer on a droppable target, reuse initial image & size + glassPane.setOverTarget(false); + glassPane.setImage(dropAdapter.getImage()); + reset(); + } + + // This triggers a repaint of glassPane + glassPane.setPoint(screenPoint); + } + + public final void reset () + { + prevComponent = new WeakReference<>(null); + } + } +} diff --git a/src/main/omr/glyph/ui/ShapeColorChooser.java b/src/main/omr/glyph/ui/ShapeColorChooser.java new file mode 100644 index 0000000..327f8a0 --- /dev/null +++ b/src/main/omr/glyph/ui/ShapeColorChooser.java @@ -0,0 +1,555 @@ +//----------------------------------------------------------------------------// +// // +// S h a p e C o l o r C h o o s e r // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.glyph.ui; + +import omr.glyph.Shape; +import omr.glyph.ShapeSet; + +import omr.ui.MainGui; + +import org.jdesktop.application.Application; +import org.jdesktop.application.ResourceMap; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.awt.BorderLayout; +import java.awt.Color; +import java.awt.Dimension; +import java.awt.Font; +import java.awt.GridLayout; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; + +import javax.swing.AbstractAction; +import javax.swing.BorderFactory; +import javax.swing.JButton; +import javax.swing.JColorChooser; +import javax.swing.JFrame; +import javax.swing.JLabel; +import javax.swing.JMenuItem; +import javax.swing.JPanel; +import javax.swing.JPopupMenu; +import javax.swing.WindowConstants; +import javax.swing.event.ChangeEvent; +import javax.swing.event.ChangeListener; + +/** + * Class {@code ShapeColorChooser} offers a convenient user interface + * to choose proper color for each glyph shape. + * It is derived from the Sun Java tutorial ColorChooserDemo. + * + *

The right part of the panel is used by a classic color chooser + * + *

The left part of the panel is used by the shape at hand, with a way to + * browse through the various defined shapes. + * + *

The strategy is the following: First the predefined shape ranges (such as + * "Physicals", "Bars", "Clefs", ...) have their own color defined. Then, each + * individual shape within these shape ranges has its color assigned by default + * to the color of the containing range, unless a color is specifically assigned + * to this individual shape. + * + * @author Hervé Bitteur + */ +public class ShapeColorChooser + implements ChangeListener +{ + //~ Static fields/initializers --------------------------------------------- + + /** Usual logger utility */ + private static final Logger logger = LoggerFactory.getLogger( + ShapeColorChooser.class); + + private static JFrame frame; + + //~ Instance fields -------------------------------------------------------- + /** The classic color chooser utility */ + private JColorChooser colorChooser; + + /** Color chosen in the JColorChooser utility */ + private Color chosenColor; + + /** UI component */ + private JPanel component; + + /** To select shape range */ + private RangesPane ranges; + + /** To select shape (within selected range) */ + private ShapesPane shapes; + + //~ Constructors ----------------------------------------------------------- + //-------------------// + // ShapeColorChooser // + //-------------------// + /** + * Create an instance of ShapeColorChooser (should be improved to always + * reuse the same instance. TODO) + */ + private ShapeColorChooser () + { + component = new JPanel(new BorderLayout()); + + // Range Panel + ranges = new RangesPane(); + shapes = new ShapesPane(); + + // Global Panel + JPanel globalPanel = new JPanel(new BorderLayout()); + globalPanel.add(ranges, BorderLayout.NORTH); + globalPanel.add(shapes, BorderLayout.SOUTH); + globalPanel.setPreferredSize(new Dimension(400, 400)); + + // Color chooser + colorChooser = new JColorChooser(); + colorChooser.getSelectionModel() + .addChangeListener(this); + colorChooser.setBorder( + BorderFactory.createTitledBorder("Choose Shape Color")); + + component.add(globalPanel, BorderLayout.CENTER); + component.add(colorChooser, BorderLayout.EAST); + } + + //~ Methods ---------------------------------------------------------------- + //-----------// + // showFrame // + //-----------// + /** + * Display the UI frame. + */ + public static void showFrame () + { + if (frame == null) { + frame = new JFrame(); + frame.setName("shapeColorChooserFrame"); + frame.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE); + + frame.add(new ShapeColorChooser().component); + + // Resources injection + ResourceMap resource = Application.getInstance() + .getContext() + .getResourceMap( + ShapeColorChooser.class); + resource.injectComponents(frame); + } + + MainGui.getInstance() + .show(frame); + } + + //--------------// + // stateChanged // + //--------------// + /** + * Triggered when color selection in the color chooser has changed. + * + * @param e not used + */ + @Override + public void stateChanged (ChangeEvent e) + { + chosenColor = colorChooser.getColor(); + ///logger.info("chosenColor: " + chosenColor); + ranges.colorChanged(); + shapes.colorChanged(); + } + + //~ Inner Classes ---------------------------------------------------------- + //------// + // Pane // + //------// + private abstract class Pane + extends JPanel + { + //~ Instance fields ---------------------------------------------------- + + public JLabel banner = new JLabel("", JLabel.CENTER); + + public JPopupMenu menu = new JPopupMenu(); + + //~ Constructors ------------------------------------------------------- + public Pane (String title) + { + setLayout(new BorderLayout()); + setBorder(BorderFactory.createTitledBorder(title)); + + banner.setForeground(Color.black); + banner.setBackground(Color.white); + banner.setOpaque(true); + banner.setFont(new Font("SansSerif", Font.BOLD, 18)); + add(banner, BorderLayout.CENTER); + } + + //~ Methods ------------------------------------------------------------ + public abstract void colorChanged (); + + protected abstract void refreshBanner (); + } + + //------------// + // RangesPane // + //------------// + private class RangesPane + extends Pane + { + //~ Instance fields ---------------------------------------------------- + + public ShapeSet current; + + private SelectAction select = new SelectAction(); + + private JButton selectButton = new JButton(select); + + private PasteAction paste = new PasteAction(); + + private ActionListener selectionListener = new ActionListener() + { + // Called when a range has been selected + @Override + public void actionPerformed (ActionEvent e) + { + JMenuItem source = (JMenuItem) e.getSource(); + current = ShapeSet.valueOf(source.getText()); + + if (current != null) { + banner.setText(current.getName()); + + Color color = current.getColor(); + + if (color != null) { + //colorChooser.setColor(color); + refreshBanner(); + } else { + banner.setForeground(Color.black); + } + + paste.setEnabled(false); + shapes.setRange(); + } else { + banner.setText(""); + } + } + }; + + private JButton pasteButton = new JButton(paste); + + //~ Constructors ------------------------------------------------------- + public RangesPane () + { + super("Shape Range"); + + add(selectButton, BorderLayout.NORTH); + add(pasteButton, BorderLayout.SOUTH); + + paste.setEnabled(false); + + buildRangesMenu(); + } + + //~ Methods ------------------------------------------------------------ + // When color chooser selection has been made + @Override + public void colorChanged () + { + if (current != null) { + paste.setEnabled(true); + } + } + + @Override + protected void refreshBanner () + { + if (current != null) { + banner.setForeground(current.getColor()); + } + } + + private void buildRangesMenu () + { + menu.removeAll(); + ShapeSet.addAllShapeSets(menu, selectionListener); + } + + //~ Inner Classes ------------------------------------------------------ + private class PasteAction + extends AbstractAction + { + //~ Constructors --------------------------------------------------- + + public PasteAction () + { + super("Paste"); + } + + //~ Methods -------------------------------------------------------- + @Override + public void actionPerformed (ActionEvent e) + { + current.setConstantColor(chosenColor); + setEnabled(false); + refreshBanner(); + buildRangesMenu(); + + // Forward to contained shapes ? + shapes.setRange(); + } + } + + private class SelectAction + extends AbstractAction + { + //~ Constructors --------------------------------------------------- + + public SelectAction () + { + super("Select"); + } + + //~ Methods -------------------------------------------------------- + @Override + public void actionPerformed (ActionEvent e) + { + JButton button = (JButton) e.getSource(); + menu.show(RangesPane.this, button.getX(), button.getY()); + } + } + } + + //------------// + // ShapesPane // + //------------// + private class ShapesPane + extends Pane + { + //~ Instance fields ---------------------------------------------------- + + public Shape current; + + private ActionListener selectionListener = new ActionListener() + { + // Called when a shape has been selected + @Override + public void actionPerformed (ActionEvent e) + { + JMenuItem source = (JMenuItem) e.getSource(); + current = Shape.valueOf(source.getText()); + banner.setText(current.toString()); + + // Check if a specific color is assigned + Color color = current.getColor(); + + if (color == ranges.current.getColor()) { + prepareDefaultOption(); + refreshBanner(); + } else { + prepareSpecificOption(); + banner.setForeground(ranges.current.getColor()); + } + } + }; + + private CopyAction copy = new CopyAction(); + + private CutAction cut = new CutAction(); + + private PasteAction paste = new PasteAction(); + + private SelectAction select = new SelectAction(); + + private JButton selectButton = new JButton(select); + + private boolean isSpecific; + + //~ Constructors ------------------------------------------------------- + public ShapesPane () + { + super("Individual Shape"); + + selectButton.setText("-- Select Shape --"); + // No range has been selected yet, so no shape can be selected + select.setEnabled(false); + + add(selectButton, BorderLayout.NORTH); + + // A series of 3 buttons at the bottom + JPanel buttons = new JPanel(new GridLayout(1, 3)); + buttons.add(new JButton(cut)); + buttons.add(new JButton(copy)); + buttons.add(new JButton(paste)); + add(buttons, BorderLayout.SOUTH); + + cut.setEnabled(false); + copy.setEnabled(false); + paste.setEnabled(false); + } + + //~ Methods ------------------------------------------------------------ + // When color chooser selection has been made + @Override + public void colorChanged () + { + updateActions(); + } + + // When a new range has been selected + public void setRange () + { + buildShapesMenu(); + + select.setEnabled(true); + selectButton.setText( + "-- Select Shape from " + ranges.current.getName() + " --"); + + current = null; + banner.setText(""); + cut.setEnabled(false); + copy.setEnabled(false); + paste.setEnabled(false); + } + + @Override + protected void refreshBanner () + { + if (current != null) { + banner.setForeground(current.getColor()); + } + } + + private void buildShapesMenu () + { + menu.removeAll(); + + // Add all shapes within current range + ShapeSet.addSetShapes(ranges.current, menu, selectionListener); + } + + private void prepareDefaultOption () + { + isSpecific = true; + updateActions(); + } + + private void prepareSpecificOption () + { + isSpecific = false; + updateActions(); + + if (chosenColor != null) { + paste.setEnabled(chosenColor != ranges.current.getColor()); + } else { + paste.setEnabled(false); + } + } + + private void updateActions () + { + if (current != null) { + cut.setEnabled(isSpecific); + copy.setEnabled(isSpecific); + paste.setEnabled(!isSpecific); + } else { + cut.setEnabled(false); + copy.setEnabled(false); + paste.setEnabled(false); + } + } + + //~ Inner Classes ------------------------------------------------------ + private class CopyAction + extends AbstractAction + { + //~ Constructors --------------------------------------------------- + + public CopyAction () + { + super("Copy"); + } + + //~ Methods -------------------------------------------------------- + @Override + public void actionPerformed (ActionEvent e) + { + colorChooser.setColor(current.getColor()); + } + } + + private class CutAction + extends AbstractAction + { + //~ Constructors --------------------------------------------------- + + public CutAction () + { + super("Cut"); + } + + //~ Methods -------------------------------------------------------- + @Override + public void actionPerformed (ActionEvent e) + { + // Drop specific for default + current.setColor(ranges.current.getColor()); + + prepareSpecificOption(); + + refreshBanner(); + buildShapesMenu(); + } + } + + private class PasteAction + extends AbstractAction + { + //~ Constructors --------------------------------------------------- + + public PasteAction () + { + super("Paste"); + } + + //~ Methods -------------------------------------------------------- + @Override + public void actionPerformed (ActionEvent e) + { + // Set a specific color + current.setConstantColor(chosenColor); + + prepareDefaultOption(); + + refreshBanner(); + buildShapesMenu(); + } + } + + private class SelectAction + extends AbstractAction + { + //~ Constructors --------------------------------------------------- + + public SelectAction () + { + super("Select"); + } + + //~ Methods -------------------------------------------------------- + @Override + public void actionPerformed (ActionEvent e) + { + JButton button = (JButton) e.getSource(); + menu.show(ShapesPane.this, button.getX(), button.getY()); + } + } + } +} diff --git a/src/main/omr/glyph/ui/ShapeFocusBoard.java b/src/main/omr/glyph/ui/ShapeFocusBoard.java new file mode 100644 index 0000000..dd02bcb --- /dev/null +++ b/src/main/omr/glyph/ui/ShapeFocusBoard.java @@ -0,0 +1,515 @@ +//----------------------------------------------------------------------------// +// // +// S h a p e F o c u s B o a r d // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.glyph.ui; + +import omr.constant.Constant; +import omr.constant.ConstantSet; + +import omr.glyph.GlyphRegression; +import omr.glyph.Shape; +import omr.glyph.ShapeDescription; +import omr.glyph.ShapeSet; +import omr.glyph.facets.Glyph; + +import omr.math.LinearEvaluator.Printer; + +import omr.selection.GlyphEvent; +import omr.selection.GlyphIdEvent; +import omr.selection.SelectionHint; +import omr.selection.UserEvent; + +import omr.sheet.Sheet; + +import omr.ui.Board; +import omr.ui.field.SpinnerUtil; +import static omr.ui.field.SpinnerUtil.*; +import omr.ui.util.Panel; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.jgoodies.forms.builder.PanelBuilder; +import com.jgoodies.forms.layout.CellConstraints; +import com.jgoodies.forms.layout.FormLayout; + +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 javax.swing.JButton; +import javax.swing.JComboBox; +import javax.swing.JLabel; +import javax.swing.JMenuItem; +import javax.swing.JPopupMenu; +import javax.swing.JSpinner; +import javax.swing.SpinnerListModel; +import javax.swing.SwingConstants; +import javax.swing.event.ChangeEvent; +import javax.swing.event.ChangeListener; + +/** + * Class {@code ShapeFocusBoard} handles a user iteration within a + * collection of glyphs. + * The collection may be built from glyphs of a given shape, + * or from glyphs similar to a given glyph, etc. + * + * @author Hervé Bitteur + */ +public class ShapeFocusBoard + extends Board +{ + //~ Static fields/initializers --------------------------------------------- + + /** Specific application parameters */ + private static final Constants constants = new Constants(); + + /** Usual logger utility */ + private static final Logger logger = LoggerFactory.getLogger( + ShapeFocusBoard.class); + + /** Events this board is interested in */ + private static final Class[] eventsRead = new Class[]{GlyphEvent.class}; + + //~ Enumerations ----------------------------------------------------------- + /** Filter on which symbols should be displayed */ + private static enum Filter + { + //~ Enumeration constant initializers ---------------------------------- + + /** Display all symbols */ + ALL, + /** Display only known symbols */ + KNOWN, + /** Display only unknown symbols */ + UNKNOWN, + /** Display only translated + * symbols */ + TRANSLATED, + /** Display only untranslated + * symbols */ + UNTRANSLATED; + + } + + //~ Instance fields -------------------------------------------------------- + private final Sheet sheet; + + /** Browser on the collection of glyphs */ + private Browser browser = new Browser(); + + /** Button to select the shape focus */ + private JButton selectButton = new JButton(); + + /** Filter for known / unknown symbol display */ + private JComboBox filterButton = new JComboBox<>( + Filter.values()); + + /** Popup menu to allow shape selection */ + private JPopupMenu pm = new JPopupMenu(); + + //~ Constructors ----------------------------------------------------------- + //-----------------// + // ShapeFocusBoard // + //-----------------// + /** + * Create the instance to handle the shape focus, with pointers to + * needed companions. + * + * @param sheet the related sheet + * @param controller the related glyph controller + * @param filterListener the action linked to filter button + */ + public ShapeFocusBoard (Sheet sheet, + GlyphsController controller, + ActionListener filterListener, + boolean expanded) + { + super( + Board.FOCUS, + controller.getNest().getGlyphService(), + eventsRead, + false, + expanded); + + this.sheet = sheet; + + // Tool Tips + selectButton.setToolTipText("Select candidate shape"); + selectButton.setHorizontalAlignment(SwingConstants.LEFT); + selectButton.addActionListener( + new ActionListener() + { + @Override + public void actionPerformed (ActionEvent e) + { + pm.show( + selectButton, + selectButton.getX(), + selectButton.getY()); + } + }); + + // Filter + filterButton.addActionListener(filterListener); + filterButton.setToolTipText( + "Select displayed glyphs according to their current state"); + + // Popup menu for shape selection + JMenuItem noFocus = new JMenuItem("No Focus"); + noFocus.setToolTipText("Cancel any focus"); + noFocus.addActionListener( + new ActionListener() + { + @Override + public void actionPerformed (ActionEvent e) + { + setCurrentShape(null); + } + }); + pm.add(noFocus); + ShapeSet.addAllShapes( + pm, + new ActionListener() + { + @Override + public void actionPerformed (ActionEvent e) + { + JMenuItem source = (JMenuItem) e.getSource(); + setCurrentShape(Shape.valueOf(source.getText())); + } + }); + + defineLayout(); + + // Initially, no focus + setCurrentShape(null); + } + + //~ Methods ---------------------------------------------------------------- + //-------------// + // isDisplayed // + //-------------// + /** + * Report whether the glyph at hand is to be displayed, according to + * the current filter + * + * @param glyph the glyph at hand, perhaps null + * @return true if to be displayed + */ + public boolean isDisplayed (Glyph glyph) + { + switch ((Filter) filterButton.getSelectedItem()) { + case KNOWN: + return (glyph != null) && glyph.isKnown(); + + case UNKNOWN: + return (glyph == null) || !glyph.isKnown(); + + case TRANSLATED: + return (glyph != null) && glyph.isKnown() && glyph.isTranslated(); + + case UNTRANSLATED: + return (glyph != null) && glyph.isKnown() && !glyph.isTranslated(); + + default: + case ALL: + return true; + } + } + + //---------// + // onEvent // + //---------// + /** + * Notification about selection objects. + * We used to use it on a just modified glyph, to set the new shape focus + * But this conflicts with the ability to browse a collection of similar + * glyphs and assign them on the fly + * + * @param event the notified event + */ + @Override + public void onEvent (UserEvent event) + { + // Empty + } + + //-----------------// + // setCurrentShape // + //-----------------// + /** + * Define the new current shape. + * + * @param currentShape the shape to be considered as current + */ + public void setCurrentShape (Shape currentShape) + { + browser.resetIds(); + + if (currentShape != null) { + // Update the shape button + selectButton.setText(currentShape.toString()); + selectButton.setIcon(currentShape.getDecoratedSymbol()); + + // Count the number of glyphs assigned to current shape + for (Glyph glyph : sheet.getActiveGlyphs()) { + if (glyph.getShape() == currentShape) { + browser.addId(glyph.getId()); + } + } + + setSelected(true); + setVisible(true); + } else { + // Void the shape button + selectButton.setText("- No Focus -"); + selectButton.setIcon(null); + } + + browser.refresh(); + } + + //-----------------// + // setSimilarGlyph // + //-----------------// + /** + * Define the glyphs collection as all glyphs whose physical + * appearance is "similar" to the appearance of the provided glyph + * example. + * + * @param example the provided example + */ + public void setSimilarGlyph (Glyph example) + { + browser.resetIds(); + + if (example != null) { + GlyphRegression evaluator = GlyphRegression.getInstance(); + double[] pattern = ShapeDescription.features(example); + List pairs = new ArrayList<>(); + + // Retrieve the glyphs similar to the example + for (Glyph glyph : sheet.getActiveGlyphs()) { + double dist = evaluator.measureDistance(glyph, pattern); + pairs.add(new DistIdPair(dist, glyph.getId())); + } + + Collections.sort(pairs, DistIdPair.distComparator); + + for (DistIdPair pair : pairs) { + browser.addId(pair.id); + } + + // To get a detailed table of the distances (debugging) + if (constants.printDistances.getValue()) { + Printer printer = evaluator.getEngine().new Printer(11); + String indent = " "; + System.out.println(indent + printer.getDefaults()); + System.out.println(indent + printer.getNames()); + System.out.println(indent + printer.getDashes()); + + for (DistIdPair pair : pairs) { + Glyph glyph = sheet.getVerticalsController() + .getGlyphById(pair.id); + double[] gPat = ShapeDescription.features(glyph); + Shape shape = glyph.getShape(); + System.out.printf( + "%18s", + (shape != null) ? shape.toString() : ""); + System.out.println(printer.getDeltas(gPat, pattern)); + System.out.printf("g#%04d d:%9f", pair.id, pair.dist); + System.out.println( + printer.getWeightedDeltas(gPat, pattern)); + } + } + + // Update the shape button + selectButton.setText("Glyphs similar to #" + example.getId()); + selectButton.setIcon(null); + + setSelected(true); + setVisible(true); + } else { + // Void the shape button + selectButton.setText("- No Focus -"); + selectButton.setIcon(null); + } + + browser.refresh(); + } + + //--------------// + // defineLayout // + //--------------// + private void defineLayout () + { + final String fieldInterline = Panel.getFieldInterline(); + + String colSpec = Panel.makeColumns(3); + FormLayout layout = new FormLayout( + colSpec, + "pref," + fieldInterline + "," + "pref"); + + PanelBuilder builder = new PanelBuilder(layout, getBody()); + builder.setDefaultDialogBorder(); + + CellConstraints cst = new CellConstraints(); + + int r = 1; // -------------------------------- + builder.add(browser.count, cst.xy(1, r)); + builder.add(browser.spinner, cst.xy(3, r)); + builder.add(selectButton, cst.xywh(7, r, 5, 3)); + + r += 2; // -------------------------------- + builder.add(filterButton, cst.xyw(1, r, 3)); + } + + //~ Inner Classes ---------------------------------------------------------- + //------------// + // DistIdPair // + //------------// + /** + * Needed to sort glyphs id according to their distance + */ + private static class DistIdPair + { + //~ Static fields/initializers ----------------------------------------- + + private static final Comparator distComparator = new Comparator() + { + @Override + public int compare (DistIdPair o1, + DistIdPair o2) + { + return Double.compare(o1.dist, o2.dist); + } + }; + + //~ Instance fields ---------------------------------------------------- + final double dist; + + final int id; + + //~ Constructors ------------------------------------------------------- + public DistIdPair (double dist, + int id) + { + this.dist = dist; + this.id = id; + } + + //~ Methods ------------------------------------------------------------ + @Override + public String toString () + { + return "dist:" + dist + " glyph#" + id; + } + } + + //---------// + // Browser // + //---------// + private class Browser + implements ChangeListener + { + //~ Instance fields ---------------------------------------------------- + + // Spinner on these glyphs + ArrayList ids = new ArrayList<>(); + + // Number of glyphs + JLabel count = new JLabel("", SwingConstants.RIGHT); + + JSpinner spinner = new JSpinner(new SpinnerListModel()); + + //~ Constructors ------------------------------------------------------- + //---------// + // Browser // + //---------// + public Browser () + { + resetIds(); + spinner.addChangeListener(this); + SpinnerUtil.setList(spinner, ids); + refresh(); + } + + //~ Methods ------------------------------------------------------------ + //-------// + // addId // + //-------// + public void addId (int id) + { + ids.add(id); + } + + //---------// + // refresh // + //---------// + public void refresh () + { + if (ids.size() > 1) { // To skip first NO_VALUE item + count.setText(0 + "/" + (ids.size() - 1)); + spinner.setEnabled(true); + } else { + count.setText(""); + spinner.setEnabled(false); + } + + spinner.setValue(NO_VALUE); + } + + //----------// + // resetIds // + //----------// + public void resetIds () + { + ids.clear(); + ids.add(NO_VALUE); + } + + //--------------// + // stateChanged // + //--------------// + @Override + public void stateChanged (ChangeEvent e) + { + int id = (Integer) spinner.getValue(); + + int index = ids.indexOf(id); + count.setText(index + "/" + (ids.size() - 1)); + + if (id != NO_VALUE) { + getSelectionService() + .publish( + new GlyphIdEvent(this, SelectionHint.GLYPH_INIT, null, id)); + } + } + } + + //-----------// + // Constants // + //-----------// + private static final class Constants + extends ConstantSet + { + //~ Instance fields ---------------------------------------------------- + + Constant.Boolean printDistances = new Constant.Boolean( + false, + "Should we print out distance details when looking for similar glyphs?"); + + } +} diff --git a/src/main/omr/glyph/ui/SpinnerGlyphModel.java b/src/main/omr/glyph/ui/SpinnerGlyphModel.java new file mode 100644 index 0000000..8a877f0 --- /dev/null +++ b/src/main/omr/glyph/ui/SpinnerGlyphModel.java @@ -0,0 +1,229 @@ +//----------------------------------------------------------------------------// +// // +// S p i n n e r G l y p h M o d e l // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.glyph.ui; + +import omr.glyph.Nest; +import omr.glyph.facets.Glyph; +import static omr.ui.field.SpinnerUtil.*; + +import omr.util.Predicate; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.swing.AbstractSpinnerModel; + +/** + * Class {@code SpinnerGlyphModel} is a spinner model backed by a + * {@link Nest}. + * Any modification in the nest is thus transparently handled, since the nest + * is the model. + *

A glyph {@link Predicate} can be assigned to this SpinnerGlyphModel at + * construction time in order to restrict the population of glyphs in the + * spinner. + * This class is used by {@link GlyphBoard} only, but is not coupled with it. + * + * @author Hervé Bitteur + */ +public class SpinnerGlyphModel + extends AbstractSpinnerModel +{ + //~ Static fields/initializers --------------------------------------------- + + /** Usual logger utility */ + private static final Logger logger = LoggerFactory.getLogger( + SpinnerGlyphModel.class); + + //~ Instance fields -------------------------------------------------------- + /** Underlying glyph nest */ + private final Nest nest; + + /** Additional predicate if any */ + private final Predicate predicate; + + /** Current glyph id */ + private Integer currentId; + + //~ Constructors ----------------------------------------------------------- + //-------------------// + // SpinnerGlyphModel // + //-------------------// + /** + * Creates a new SpinnerGlyphModel object, on all nest glyphs + * + * @param nest the underlying glyph nest + */ + public SpinnerGlyphModel (Nest nest) + { + this(nest, null); + } + + //-------------------// + // SpinnerGlyphModel // + //-------------------// + /** + * Creates a new SpinnerGlyphModel object, with a related glyph predicate + * + * @param nest the underlying glyph nest + * @param predicate predicate of glyph, or null + */ + public SpinnerGlyphModel (Nest nest, + Predicate predicate) + { + if (nest == null) { + throw new IllegalArgumentException( + "SpinnerGlyphModel expects non-null glyph nest"); + } + + this.nest = nest; + this.predicate = predicate; + + currentId = NO_VALUE; + } + + //~ Methods ---------------------------------------------------------------- + //--------------// + // getNextValue // + //--------------// + /** + * Return the next legal glyph id in the sequence that comes after the glyph + * id returned by {@code getValue()}. If the end of the sequence has + * been reached then return null. + * + * @return the next legal glyph id or null if one doesn't exist + */ + @Override + public Object getNextValue () + { + final int cur = currentId.intValue(); + logger.debug("getNextValue cur={}", cur); + + if (cur == NO_VALUE) { + // Return first suitable glyph in nest + for (Glyph glyph : nest.getAllGlyphs()) { + if ((predicate == null) || predicate.check(glyph)) { + return glyph.getId(); + } + } + + return null; + } else { + // Return first suitable glyph after current glyph in nest + boolean found = false; + + for (Glyph glyph : nest.getAllGlyphs()) { + if (!found) { + if (glyph.getId() == cur) { + found = true; + } + } else if ((predicate == null) || predicate.check(glyph)) { + return glyph.getId(); + } + } + + return null; + } + } + + //------------------// + // getPreviousValue // + //------------------// + /** + * Return the legal glyph id in the sequence that comes before the glyph id + * returned by {@code getValue()}. If the end of the sequence has been + * reached then return null. + * + * @return the previous legal value or null if one doesn't exist + */ + @Override + public Object getPreviousValue () + { + Glyph prevGlyph = null; + final int cur = currentId.intValue(); + logger.debug("getPreviousValue cur={}", cur); + + if (cur == NO_VALUE) { + return NO_VALUE; + } + + // Nest + for (Glyph glyph : nest.getAllGlyphs()) { + if (glyph.getId() == cur) { + return (prevGlyph != null) ? prevGlyph.getId() : NO_VALUE; + } + + // Should we remember this as (suitable) previous glyph ? + if ((predicate == null) || predicate.check(glyph)) { + prevGlyph = glyph; + } + } + + return null; + } + + //----------// + // getValue // + //----------// + /** + * The current element of the sequence. + * + * @return the current spinner value. + */ + @Override + public Object getValue () + { + logger.debug("getValue currentId={}", currentId); + + return currentId; + } + + //----------// + // setValue // + //----------// + /** + * Changes current glyph id of the model. If the glyph id is illegal then + * an {@code IllegalArgumentException} is thrown. + * + * @param value the value to set + * @exception IllegalArgumentException if {@code value} isn't allowed + */ + @Override + public void setValue (Object value) + { + logger.debug("setValue value={}", value); + + Integer id = (Integer) value; + boolean ok = false; + + if (id == NO_VALUE) { + ok = true; + } else { + // Nest + Glyph glyph = nest.getGlyph(id); + + if (glyph != null) { + if (predicate != null) { + ok = predicate.check(glyph); + } else { + ok = true; + } + } + } + + if (ok) { + currentId = id; + fireStateChanged(); + } else { + logger.warn("Invalid glyph id: {}", id); + } + } +} diff --git a/src/main/omr/glyph/ui/SymbolGlyphBoard.java b/src/main/omr/glyph/ui/SymbolGlyphBoard.java new file mode 100644 index 0000000..969ccd6 --- /dev/null +++ b/src/main/omr/glyph/ui/SymbolGlyphBoard.java @@ -0,0 +1,466 @@ +//----------------------------------------------------------------------------// +// // +// S y m b o l G l y p h B o a r d // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.glyph.ui; + +import omr.glyph.Shape; +import omr.glyph.ShapeSet; +import omr.glyph.facets.Glyph; +import omr.text.TextRole; + +import omr.score.entity.Text.CreatorText.CreatorType; +import omr.score.entity.TimeRational; +import omr.score.entity.TimeSignature; + +import omr.selection.GlyphEvent; +import omr.selection.GlyphSetEvent; +import omr.selection.MouseMovement; +import omr.selection.UserEvent; + +import omr.sheet.ui.SheetsController; + +import omr.text.TextRoleInfo; +import omr.text.TextWord; + +import omr.ui.field.LComboBox; +import omr.ui.field.LDoubleField; +import omr.ui.field.LIntegerField; +import omr.ui.field.LTextField; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.awt.event.ActionEvent; +import java.util.Set; + +import javax.swing.AbstractAction; +import javax.swing.Action; +import javax.swing.JComponent; +import javax.swing.JTextField; +import javax.swing.KeyStroke; + +/** + * Class {@code SymbolGlyphBoard} defines an extended glyph board, + * with characteristics (pitch position, stem number, etc) that are + * specific to a symbol, and an additional symbol glyph spinner. + *

    + *
  • A symbolSpinner to browse through all glyphs that are + * considered as symbols, that is built from aggregation of contiguous + * sections, or by combination of other symbols. + * Glyphs whose shape is set to {@link omr.glyph.Shape#NOISE}, that is too + * small glyphs, are not included in this spinner.
+ * + *

Layout of an instance of SymbolGlyphBoard:
+ * + *

+ * + * @author Hervé Bitteur + */ +public class SymbolGlyphBoard + extends GlyphBoard +{ + //~ Static fields/initializers --------------------------------------------- + + /** Usual logger utility */ + private static final Logger logger = LoggerFactory.getLogger( + SymbolGlyphBoard.class); + + //~ Instance fields -------------------------------------------------------- + /** Numerator of time signature */ + private LIntegerField timeNum; + + /** Denominator of time signature */ + private LIntegerField timeDen; + + /** ComboBox for text role */ + private LComboBox roleCombo; + + /** ComboBox for text role type */ + private LComboBox typeCombo; + + /** Output : textual confidence */ + protected LIntegerField confField; + + /** Input/Output : textual content */ + protected LTextField textField; + + /** Glyph characteristics : position wrt staff */ + private LDoubleField pitchPosition = new LDoubleField( + false, + "Pitch", + "Logical pitch position", + "%.3f"); + + /** Glyph characteristics : is there a ledger */ + private LTextField ledger = new LTextField( + false, + "Ledger", + "Does this glyph intersect a ledger?"); + + /** Glyph characteristics : how many stems */ + private LIntegerField stems = new LIntegerField( + false, + "Stems", + "Number of stems connected to this glyph"); + + /** Glyph characteristics : normalized weight */ + private LDoubleField weight = new LDoubleField( + false, + "Weight", + "Normalized weight", + "%.3f"); + + /** Glyph characteristics : normalized width */ + private LDoubleField width = new LDoubleField( + false, + "Width", + "Normalized width", + "%.3f"); + + /** Glyph characteristics : normalized height */ + private LDoubleField height = new LDoubleField( + false, + "Height", + "Normalized height", + "%.3f"); + + /** Handling of entered / selected values */ + private final Action paramAction; + + /** To avoid unwanted events */ + private boolean selfUpdatingText; + + //~ Constructors ----------------------------------------------------------- + //------------------// + // SymbolGlyphBoard // + //------------------// + /** + * Create the symbol glyph board. + * + * @param glyphsController the companion which handles glyph (de)assignments + * @param useSpinners true for use of spinners + * @param expanded true to initially expand this board + */ + public SymbolGlyphBoard (GlyphsController glyphsController, + boolean useSpinners, + boolean expanded) + { + // For all glyphs + super(glyphsController, useSpinners, true); + + // Additional combo for text role + paramAction = new ParamAction(); + roleCombo = new LComboBox<>( + "Role", + "Role of the Text", + TextRole.values()); + roleCombo.getField().setMaximumRowCount(TextRole.values().length); + roleCombo.addActionListener(paramAction); + + // Additional combo for text type + typeCombo = new LComboBox<>( + "Type", + "Type of the Text", + CreatorType.values()); + typeCombo.addActionListener(paramAction); + + // Confidence and Text fields + confField = new LIntegerField(false, "Conf", "Confidence in text value"); + textField = new LTextField(true, "Text", "Content of a textual glyph"); + textField.getField().setHorizontalAlignment(JTextField.LEFT); + + // Time signature + timeNum = new LIntegerField("Num", ""); + timeDen = new LIntegerField("Den", ""); + + defineSpecificLayout(); + + // Needed to process user input when RETURN/ENTER is pressed + getComponent(). + getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT). + put(KeyStroke.getKeyStroke("ENTER"), "TextAction"); + getComponent().getActionMap().put("TextAction", paramAction); + } + + //~ Methods ---------------------------------------------------------------- + //---------// + // onEvent // + //---------// + /** + * Call-back triggered when Glyph Selection has been modified. + * + * @param event the (Glyph or glyph set) Selection + */ + @Override + public void onEvent (UserEvent event) + { + try { + // Ignore RELEASING + if (event.movement == MouseMovement.RELEASING) { + return; + } + + super.onEvent(event); + + if (event instanceof GlyphEvent) { + selfUpdating = true; + + GlyphEvent glyphEvent = (GlyphEvent) event; + Glyph glyph = glyphEvent.getData(); + Shape shape = (glyph != null) ? glyph.getShape() : null; + + // Fill symbol characteristics + if (glyph != null) { + pitchPosition.setValue(glyph.getPitchPosition()); + ledger.setText(Boolean.toString(glyph.isWithLedger())); + stems.setValue(glyph.getStemNumber()); + + weight.setValue(glyph.getNormalizedWeight()); + width.setValue(glyph.getNormalizedWidth()); + height.setValue(glyph.getNormalizedHeight()); + } else { + ledger.setText(""); + pitchPosition.setText(""); + stems.setText(""); + + weight.setText(""); + width.setText(""); + height.setText(""); + } + + // Text info + if (roleCombo != null) { + if ((shape != null) && shape.isText()) { + selfUpdatingText = true; + confField.setVisible(false); + textField.setVisible(true); + roleCombo.setVisible(true); + typeCombo.setVisible(false); + + roleCombo.setEnabled(true); + textField.setEnabled(true); + + if (glyph.getTextValue() != null) { + textField.setText(glyph.getTextValue()); + // Related word? + TextWord word = glyph.getTextWord(); + if (word != null) { + confField.setValue(word.getConfidence()); + confField.setVisible(true); + } + } else { + textField.setText(""); + } + + + if (glyph.getTextRole() != null) { + roleCombo.setSelectedItem(glyph.getTextRole().role); + + if (glyph.getTextRole().role == TextRole.Creator) { + typeCombo.setVisible(true); + typeCombo.setSelectedItem( + glyph.getTextRole().creatorType); + } + } else { + roleCombo.setSelectedItem(TextRole.UnknownRole); + } + + selfUpdatingText = false; + } else { + confField.setVisible(false); + textField.setVisible(false); + roleCombo.setVisible(false); + typeCombo.setVisible(false); + } + } + + // Time Signature info + if (timeNum != null) { + if (ShapeSet.Times.contains(shape)) { + timeNum.setVisible(true); + timeDen.setVisible(true); + + timeNum.setEnabled( + shape == Shape.CUSTOM_TIME); + timeDen.setEnabled( + shape == Shape.CUSTOM_TIME); + + TimeRational timeRational = (shape == Shape.CUSTOM_TIME) + ? glyph.getTimeRational() + : TimeSignature.rationalOf( + shape); + + if (timeRational != null) { + timeNum.setValue(timeRational.num); + timeDen.setValue(timeRational.den); + } else { + timeNum.setText(""); + timeDen.setText(""); + } + } else { + timeNum.setVisible(false); + timeDen.setVisible(false); + } + } + + selfUpdating = false; + } + } catch (Exception ex) { + logger.warn(getClass().getName() + " onEvent error", ex); + } + } + + //----------------------// + // defineSpecificLayout // + //----------------------// + /** + * Define a specific layout for this Symbol GlyphBoard + */ + private void defineSpecificLayout () + { + int r = 1; // -------------------------------- + // Glyph --- + + r += 2; // -------------------------------- + // shape + + r += 2; // -------------------------------- + // Glyph characteristics, first line + + builder.add(pitchPosition.getLabel(), cst.xy(1, r)); + builder.add(pitchPosition.getField(), cst.xy(3, r)); + + builder.add(ledger.getLabel(), cst.xy(5, r)); + builder.add(ledger.getField(), cst.xy(7, r)); + + builder.add(stems.getLabel(), cst.xy(9, r)); + builder.add(stems.getField(), cst.xy(11, r)); + + r += 2; // -------------------------------- + // Glyph characteristics, second line + + builder.add(weight.getLabel(), cst.xy(1, r)); + builder.add(weight.getField(), cst.xy(3, r)); + + builder.add(width.getLabel(), cst.xy(5, r)); + builder.add(width.getField(), cst.xy(7, r)); + + builder.add(height.getLabel(), cst.xy(9, r)); + builder.add(height.getField(), cst.xy(11, r)); + + r += 2; // -------------------------------- + // Text information, first line + + if (textField != null) { + builder.add(confField.getLabel(), cst.xyw(1, r, 1)); + builder.add(confField.getField(), cst.xyw(3, r, 1)); + confField.setVisible(false); + builder.add(textField.getLabel(), cst.xyw(5, r, 1)); + builder.add(textField.getField(), cst.xyw(7, r, 5)); + textField.setVisible(false); + } + + // or time signature parameters + if (timeNum != null) { + builder.add(timeNum.getLabel(), cst.xy(5, r)); + builder.add(timeNum.getField(), cst.xy(7, r)); + timeNum.setVisible(false); + + builder.add(timeDen.getLabel(), cst.xy(9, r)); + builder.add(timeDen.getField(), cst.xy(11, r)); + timeDen.setVisible(false); + } + + r += 2; // -------------------------------- + // Text information, second line + + if (roleCombo != null) { + builder.add(roleCombo.getLabel(), cst.xyw(1, r, 1)); + builder.add(roleCombo.getField(), cst.xyw(3, r, 3)); + roleCombo.setVisible(false); + + builder.add(typeCombo.getLabel(), cst.xyw(7, r, 1)); + builder.add(typeCombo.getField(), cst.xyw(9, r, 3)); + typeCombo.setVisible(false); + } + } + + //~ Inner Classes ---------------------------------------------------------- + //-------------// + // ParamAction // + //-------------// + private class ParamAction + extends AbstractAction + { + //~ Methods ------------------------------------------------------------ + + // Method run whenever user presses Return/Enter in one of the parameter + // fields + @Override + public void actionPerformed (ActionEvent e) + { + // Discard irrelevant action events + if (selfUpdatingText) { + return; + } + + // Get current glyph set + GlyphSetEvent glyphsEvent = (GlyphSetEvent) getSelectionService(). + getLastEvent( + GlyphSetEvent.class); + Set glyphs = (glyphsEvent != null) + ? glyphsEvent.getData() : null; + + if ((glyphs != null) && !glyphs.isEmpty()) { + // Read shape information + String shapeName = shapeField.getText(); + + if (shapeName.isEmpty()) { + return; + } + + Shape shape = Shape.valueOf(shapeName); + + // Text? + if (shape.isText()) { + logger.debug("Text=''{}'' Role={}", + textField.getText().trim(), + roleCombo.getSelectedItem()); + + CreatorType type = null; + TextRole role = roleCombo.getSelectedItem(); + if (role == TextRole.Creator) { + type = typeCombo.getSelectedItem(); + } + TextRoleInfo roleInfo = new TextRoleInfo(role, type); + SheetsController.getCurrentSheet().getSymbolsController(). + asyncAssignTexts( + glyphs, + roleInfo, + textField.getText()); + } else // Custom time sig? + if (shape == Shape.CUSTOM_TIME) { + int num = timeNum.getValue(); + int den = timeDen.getValue(); + + if ((num != 0) && (den != 0)) { + SheetsController.getCurrentSheet(). + getSymbolsController().asyncAssignRationals( + glyphs, + new TimeRational(num, den)); + } else { + logger.warn("Invalid time signature parameters"); + } + } + } + } + } +} diff --git a/src/main/omr/glyph/ui/SymbolMenu.java b/src/main/omr/glyph/ui/SymbolMenu.java new file mode 100644 index 0000000..9264e56 --- /dev/null +++ b/src/main/omr/glyph/ui/SymbolMenu.java @@ -0,0 +1,507 @@ +//----------------------------------------------------------------------------// +// // +// S y m b o l M e n u // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.glyph.ui; + +import omr.glyph.Evaluation; +import omr.glyph.Grades; +import omr.glyph.Shape; +import omr.glyph.ShapeEvaluator; +import omr.glyph.facets.Glyph; + +import omr.sheet.SystemInfo; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.awt.event.ActionEvent; +import java.util.Collections; +import java.util.Set; + +/** + * Class {@code SymbolMenu} defines the menu which is linked to + * the current selection of one or several glyphs. + * + * @author Hervé Bitteur + */ +public class SymbolMenu + extends GlyphMenu +{ + //~ Static fields/initializers --------------------------------------------- + + /** Usual logger utility */ + private static final Logger logger = LoggerFactory.getLogger( + SymbolMenu.class); + + //~ Instance fields -------------------------------------------------------- + // Links to partnering entities + private final ShapeFocusBoard shapeFocus; + + private final ShapeEvaluator evaluator; + + // To handle proposed compound shape + private Glyph proposedGlyph; + + private Shape proposedShape; + + //~ Constructors ----------------------------------------------------------- + //------------// + // SymbolMenu // + //------------// + /** + * Create the Symbol menu. + * + * @param symbolsController the top companion + * @param evaluator the glyph evaluator + * @param shapeFocus the current shape focus + */ + public SymbolMenu (final SymbolsController symbolsController, + ShapeEvaluator evaluator, + ShapeFocusBoard shapeFocus) + { + super(symbolsController); + this.evaluator = evaluator; + this.shapeFocus = shapeFocus; + } + + //~ Methods ---------------------------------------------------------------- + //-----------------// + // registerActions // + //-----------------// + @Override + protected void registerActions () + { + // Copy & Paste actions + register(0, new PasteAction()); + register(0, new CopyAction()); + + // Deassign selected glyph(s) + register(0, new DeassignAction()); + + // Manually assign a shape + register(0, new AssignAction()); + + // Build a compound, with menu for shape selection + register(0, new CompoundAction()); + + // Segment the glyph into stems & leaves + register(0, new StemSegmentAction()); + + // Segment the glyph into short stems & leaves + register(0, new ShortStemSegmentAction()); + + // Build a compound, with proposed shape + register(0, new ProposedAction()); + + // Trim large slur glyphs + register(0, new TrimSlurAction()); + + // Dump current glyph + register(0, new DumpAction()); + + // Dump current glyph text info + register(0, new DumpTextAction()); + + // Display score counterpart + register(0, new TranslationAction()); + + // Display all glyphs of the same shape + register(0, new ShapeAction()); + + // Display all glyphs similar to the current glyph + register(0, new SimilarAction()); + } + + //~ Inner Classes ---------------------------------------------------------- + //----------------// + // DumpTextAction // + //----------------// + /** + * Dump the text information of each glyph in the selected + * collection of glyphs. + */ + private class DumpTextAction + extends DynAction + { + //~ Constructors ------------------------------------------------------- + + public DumpTextAction () + { + super(40); + } + + //~ Methods ------------------------------------------------------------ + @Override + public void actionPerformed (ActionEvent e) + { + for (Glyph glyph : nest.getSelectedGlyphSet()) { + logger.info(glyph.dumpOf()); + } + } + + @Override + public void update () + { + if (glyphNb > 0) { + setEnabled(true); + + StringBuilder sb = new StringBuilder(); + sb.append("Dump text of ") + .append(glyphNb) + .append(" glyph"); + + if (glyphNb > 1) { + sb.append("s"); + } + + putValue(NAME, sb.toString()); + putValue(SHORT_DESCRIPTION, "Dump text of selected glyphs"); + } else { + setEnabled(false); + putValue(NAME, "Dump text"); + putValue(SHORT_DESCRIPTION, "No glyph to dump text"); + } + } + } + + //----------------// + // ProposedAction // + //----------------// + /** + * Accept the proposed compound with its evaluated shape. + */ + private class ProposedAction + extends DynAction + { + //~ Constructors ------------------------------------------------------- + + public ProposedAction () + { + super(30); + } + + //~ Methods ------------------------------------------------------------ + @Override + public void actionPerformed (ActionEvent e) + { + Glyph glyph = nest.getSelectedGlyph(); + + if ((glyph != null) && (glyph == proposedGlyph)) { + controller.asyncAssignGlyphs( + Collections.singleton(glyph), + proposedShape, + false); + } + } + + @Override + public void update () + { + // Proposed compound? + Glyph glyph = nest.getSelectedGlyph(); + + if ((glyphNb > 0) && (glyph != null) && (glyph.getId() == 0)) { + SystemInfo system = sheet.getSystemOf(glyph); + Evaluation vote = evaluator.vote( + glyph, + system, + Grades.symbolMinGrade); + + if (vote != null) { + proposedGlyph = glyph; + proposedShape = vote.shape; + setEnabled(true); + putValue(NAME, "Build compound as " + proposedShape); + putValue(SHORT_DESCRIPTION, "Accept the proposed compound"); + + return; + } + } + + // Nothing to propose + proposedGlyph = null; + proposedShape = null; + setEnabled(false); + putValue(NAME, "Build compound"); + putValue(SHORT_DESCRIPTION, "No proposed compound"); + } + } + + //-------------// + // ShapeAction // + //-------------// + /** + * Set the focus on all glyphs with the same shape. + */ + private class ShapeAction + extends DynAction + { + //~ Constructors ------------------------------------------------------- + + public ShapeAction () + { + super(70); + } + + //~ Methods ------------------------------------------------------------ + @Override + public void actionPerformed (ActionEvent e) + { + Set glyphs = nest.getSelectedGlyphSet(); + + if ((glyphs != null) && (glyphs.size() == 1)) { + Glyph glyph = glyphs.iterator() + .next(); + + if (glyph.getShape() != null) { + shapeFocus.setCurrentShape(glyph.getShape()); + } + } + } + + @Override + public void update () + { + Glyph glyph = nest.getSelectedGlyph(); + + if ((glyph != null) && (glyph.getShape() != null)) { + setEnabled(true); + putValue(NAME, "Show all " + glyph.getShape() + "'s"); + putValue(SHORT_DESCRIPTION, "Display all glyphs of this shape"); + } else { + setEnabled(false); + putValue(NAME, "Show all"); + putValue(SHORT_DESCRIPTION, "No shape defined"); + } + } + } + + //------------------------// + // ShortStemSegmentAction // + //------------------------// + /** + * Perform a segmentation on the selected glyphs, into short stems + * and leaves. + */ + private class ShortStemSegmentAction + extends DynAction + { + //~ Constructors ------------------------------------------------------- + + public ShortStemSegmentAction () + { + super(50); + } + + //~ Methods ------------------------------------------------------------ + @Override + public void actionPerformed (ActionEvent e) + { + Set glyphs = nest.getSelectedGlyphSet(); + ((SymbolsController) controller).asyncSegment(glyphs, true); // isShort + } + + @Override + public void update () + { + putValue(NAME, "Look for short verticals"); + + if (sheet.hasSystemBoundaries() && (glyphNb > 0) && noVirtuals) { + setEnabled(true); + putValue(SHORT_DESCRIPTION, "Extract short stems and leaves"); + } else { + setEnabled(false); + putValue(SHORT_DESCRIPTION, "No glyph to segment"); + } + } + } + + //---------------// + // SimilarAction // + //---------------// + /** + * Set the focus on all glyphs similar to the selected glyph. + */ + private class SimilarAction + extends DynAction + { + //~ Constructors ------------------------------------------------------- + + public SimilarAction () + { + super(70); + } + + //~ Methods ------------------------------------------------------------ + @Override + public void actionPerformed (ActionEvent e) + { + Set glyphs = nest.getSelectedGlyphSet(); + + if ((glyphs != null) && (glyphs.size() == 1)) { + Glyph glyph = glyphs.iterator() + .next(); + + if (glyph != null) { + shapeFocus.setSimilarGlyph(glyph); + } + } + } + + @Override + public void update () + { + Glyph glyph = nest.getSelectedGlyph(); + + if (glyph != null) { + setEnabled(true); + putValue(NAME, "Show similar glyphs"); + putValue( + SHORT_DESCRIPTION, + "Display all glyphs similar to this one"); + } else { + setEnabled(false); + putValue(NAME, "Show similar"); + putValue(SHORT_DESCRIPTION, "No glyph selected"); + } + } + } + + //-------------------// + // StemSegmentAction // + //-------------------// + /** + * Perform a segmentation on the selected glyphs, into stems and + * leaves. + */ + private class StemSegmentAction + extends DynAction + { + //~ Constructors ------------------------------------------------------- + + public StemSegmentAction () + { + super(50); + } + + //~ Methods ------------------------------------------------------------ + @Override + public void actionPerformed (ActionEvent e) + { + Set glyphs = nest.getSelectedGlyphSet(); + ((SymbolsController) controller).asyncSegment(glyphs, false); // isShort + } + + @Override + public void update () + { + putValue(NAME, "Look for verticals"); + + if (sheet.hasSystemBoundaries() && (glyphNb > 0) && noVirtuals) { + setEnabled(true); + putValue(SHORT_DESCRIPTION, "Extract stems and leaves"); + } else { + setEnabled(false); + putValue(SHORT_DESCRIPTION, "No glyph to segment"); + } + } + } + + //-------------------// + // TranslationAction // + //-------------------// + /** + * Display the score entity that translates this glyph. + */ + private class TranslationAction + extends DynAction + { + //~ Constructors ------------------------------------------------------- + + public TranslationAction () + { + super(40); + } + + //~ Methods ------------------------------------------------------------ + @Override + public void actionPerformed (ActionEvent e) + { + Set glyphs = nest.getSelectedGlyphSet(); + ((SymbolsController) controller).showTranslations(glyphs); + } + + @Override + public void update () + { + if (glyphNb > 0) { + for (Glyph glyph : nest.getSelectedGlyphSet()) { + if (glyph.isTranslated()) { + setEnabled(true); + + StringBuilder sb = new StringBuilder(); + sb.append("Show translations"); + putValue(NAME, sb.toString()); + putValue( + SHORT_DESCRIPTION, + "Show translations related to the glyph(s)"); + + return; + } + } + } + + // No translation to show + setEnabled(false); + putValue(NAME, "Translations"); + putValue(SHORT_DESCRIPTION, "No translation"); + } + } + + //----------------// + // TrimSlurAction // + //----------------// + /** + * Cleanup a glyph with focus on its slur shape. + */ + private class TrimSlurAction + extends DynAction + { + //~ Constructors ------------------------------------------------------- + + public TrimSlurAction () + { + super(60); + } + + //~ Methods ------------------------------------------------------------ + @Override + public void actionPerformed (ActionEvent e) + { + Set glyphs = nest.getSelectedGlyphSet(); + ((SymbolsController) controller).asyncTrimSlurs(glyphs); + } + + @Override + public void update () + { + putValue(NAME, "Trim slur"); + + if (sheet.hasSystemBoundaries() && (glyphNb > 0) && noVirtuals) { + setEnabled(true); + putValue(SHORT_DESCRIPTION, "Extract slur from large glyph"); + } else { + setEnabled(false); + putValue(SHORT_DESCRIPTION, "No slur to fix"); + } + } + } +} diff --git a/src/main/omr/glyph/ui/SymbolsBlackList.java b/src/main/omr/glyph/ui/SymbolsBlackList.java new file mode 100644 index 0000000..677db85 --- /dev/null +++ b/src/main/omr/glyph/ui/SymbolsBlackList.java @@ -0,0 +1,47 @@ +//----------------------------------------------------------------------------// +// // +// S y m b o l s B l a c k L i s t // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.glyph.ui; + +import omr.WellKnowns; + +import omr.util.BlackList; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Class {@code SymbolsBlackList} is a special {@link BlackList} meant + * to handle the collection of symbols as artificial glyphs. + * + * @author Hervé Bitteur + */ +public class SymbolsBlackList + extends BlackList +{ + //~ Static fields/initializers --------------------------------------------- + + /** Usual logger utility */ + private static final Logger logger = LoggerFactory.getLogger( + SymbolsBlackList.class); + + //~ Constructors ----------------------------------------------------------- + //------------------// + // SymbolsBlackList // + //------------------// + /** + * Creates a new SymbolsBlackList object. + */ + public SymbolsBlackList () + { + super(WellKnowns.SYMBOLS_FOLDER); + } +} diff --git a/src/main/omr/glyph/ui/SymbolsController.java b/src/main/omr/glyph/ui/SymbolsController.java new file mode 100644 index 0000000..fc89a17 --- /dev/null +++ b/src/main/omr/glyph/ui/SymbolsController.java @@ -0,0 +1,220 @@ +//----------------------------------------------------------------------------// +// // +// S y m b o l s C o n t r o l l e r // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.glyph.ui; + +import omr.glyph.SymbolsModel; +import omr.glyph.facets.Glyph; + +import omr.score.entity.Note; +import omr.score.entity.TimeRational; + +import omr.script.BoundaryTask; +import omr.script.RationalTask; +import omr.script.SegmentTask; +import omr.script.SlurTask; +import omr.script.TextTask; + +import omr.sheet.BrokenLineContext; +import omr.sheet.SystemBoundary; +import omr.sheet.SystemInfo; + +import omr.text.TextRoleInfo; + +import omr.util.BrokenLine; +import omr.util.VerticalSide; + +import org.jdesktop.application.Task; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.awt.Color; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Set; + +/** + * Class {@code SymbolsController} is a GlyphsController specifically + * meant for symbol glyphs, adding handling for assigning Texts, for fixing + * Slurs and for segmenting on Stems. + * + * @author Hervé Bitteur + */ +public class SymbolsController + extends GlyphsController +{ + //~ Static fields/initializers --------------------------------------------- + + /** Usual logger utility */ + private static final Logger logger = LoggerFactory.getLogger( + SymbolsController.class); + + /** Color for hiding unknown glyphs when filter is ON */ + public static final Color hiddenColor = Color.white; + + //~ Constructors ----------------------------------------------------------- + //-------------------// + // SymbolsController // + //-------------------// + /** + * Create a handler dedicated to symbol glyphs + * + * @param model the related glyphs model + */ + public SymbolsController (SymbolsModel model) + { + super(model); + } + + //~ Methods ---------------------------------------------------------------- + //----------------------// + // asyncAssignRationals // + //----------------------// + /** + * Asynchronously assign a rational value to a collection of glyphs with + * CUSTOM_TIME_SIGNATURE shape + * + * @param glyphs the impacted glyphs + * @param timeRational the time sig rational value + * @return the task that carries out the processing + */ + public Task asyncAssignRationals (Collection glyphs, + TimeRational timeRational) + { + return new RationalTask(sheet, timeRational, glyphs).launch(sheet); + } + + //------------------// + // asyncAssignTexts // + //------------------// + /** + * Asynchronously assign text characteristics to a collection of textual + * glyphs + * + * @param glyphs the impacted glyphs + * @param roleInfo the role of this textual element + * @param textContent the content as a string (if not empty) + * @return the task that carries out the processing + */ + public Task asyncAssignTexts (Collection glyphs, + TextRoleInfo roleInfo, + String textContent) + { + return new TextTask(sheet, roleInfo, textContent, glyphs). + launch(sheet); + } + + //-----------------------// + // asyncModifyBoundaries // + //-----------------------// + /** + * Asynchronously perform a modification in systems boundaries + * + * @param modifiedLines the set of modified lines + * @return the task that carries out the processing + */ + public Task asyncModifyBoundaries (Set modifiedLines) + { + List contexts = new ArrayList<>(); + + // Retrieve impacted systems + for (BrokenLine line : modifiedLines) { + int above = 0; + int below = 0; + + for (SystemInfo system : sheet.getSystems()) { + SystemBoundary boundary = system.getBoundary(); + + if (boundary.getLimit(VerticalSide.BOTTOM) == line) { + above = system.getId(); + } else if (boundary.getLimit(VerticalSide.TOP) == line) { + below = system.getId(); + } + } + + contexts.add(new BrokenLineContext(above, below, line)); + } + + return new BoundaryTask(sheet, contexts).launch(sheet); + } + + //--------------// + // asyncSegment // + //--------------// + /** + * Asynchronously segment a set of glyphs on their stems + * + * @param glyphs glyphs to segment in order to retrieve stems + * @param isShort looking for short (or standard) stems + * @return the task that carries out the processing + */ + public Task asyncSegment (Collection glyphs, + boolean isShort) + { + return new SegmentTask(sheet, isShort, glyphs).launch(sheet); + } + + //----------------// + // asyncTrimSlurs // + //----------------// + /** + * Asynchronously fix a collection of glyphs as large slurs + * + * @param glyphs the slur glyphs to fix + * @return the task that carries out the processing + */ + public Task asyncTrimSlurs (Collection glyphs) + { + return new SlurTask(sheet, glyphs).launch(sheet); + } + + //----------// + // getModel // + //----------// + /** + * Report the underlying model + * + * @return the underlying glyphs model + */ + @Override + public SymbolsModel getModel () + { + return (SymbolsModel) model; + } + + //------------------// + // showTranslations // + //------------------// + public void showTranslations (Collection glyphs) + { + for (Glyph glyph : glyphs) { + for (Object entity : glyph.getTranslations()) { + if (entity instanceof Note) { + Note note = (Note) entity; + logger.info("{}->{}", note, note.getChord()); + } else { + logger.info(entity.toString()); + } + } + } + } + + //----------// + // toString // + //----------// + @Override + public String toString () + { + return getClass().getSimpleName(); + } +} diff --git a/src/main/omr/glyph/ui/SymbolsEditor.java b/src/main/omr/glyph/ui/SymbolsEditor.java new file mode 100644 index 0000000..0090026 --- /dev/null +++ b/src/main/omr/glyph/ui/SymbolsEditor.java @@ -0,0 +1,656 @@ +//----------------------------------------------------------------------------// +// // +// S y m b o l s E d i t o r // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.glyph.ui; + +import omr.constant.ConstantSet; + +import omr.glyph.GlyphNetwork; +import omr.glyph.Glyphs; +import omr.glyph.Nest; +import omr.glyph.ShapeEvaluator; +import omr.glyph.facets.Glyph; +import omr.glyph.ui.NestView.ItemRenderer; + +import omr.lag.Lag; +import omr.lag.Section; +import omr.lag.Sections; +import omr.lag.ui.SectionBoard; + +import omr.run.RunBoard; + +import omr.score.entity.Measure; +import omr.score.entity.ScoreSystem; +import omr.score.entity.Slot; +import omr.score.entity.SystemPart; +import omr.score.ui.PageMenu; +import omr.score.ui.PagePhysicalPainter; +import omr.score.ui.PaintingParameters; + +import omr.selection.GlyphEvent; +import omr.selection.GlyphSetEvent; +import omr.selection.LocationEvent; +import omr.selection.MouseMovement; +import omr.selection.NestEvent; +import omr.selection.SectionSetEvent; +import static omr.selection.SelectionHint.*; +import omr.selection.UserEvent; + +import omr.sheet.Sheet; +import omr.sheet.SystemInfo; +import omr.sheet.ui.BoundaryEditor; +import omr.sheet.ui.PixelBoard; +import omr.sheet.ui.SheetPainter; + +import omr.step.Step; + +import omr.ui.BoardsPane; +import omr.ui.Colors; +import omr.ui.PixelCount; +import omr.ui.util.UIUtil; +import omr.ui.view.ScrollView; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.awt.Graphics2D; +import java.awt.Point; +import java.awt.Rectangle; +import java.awt.Stroke; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.beans.PropertyChangeEvent; +import java.beans.PropertyChangeListener; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Set; + +import javax.swing.JPopupMenu; +import javax.swing.SwingUtilities; + +/** + * Class {@code SymbolsEditor} defines, for a given sheet, a UI pane + * from which all symbol processing actions can be launched and their + * results checked. + * + * @author Hervé Bitteur + */ +public class SymbolsEditor + implements PropertyChangeListener +{ + //~ Static fields/initializers --------------------------------------------- + + /** Specific application parameters */ + private static final Constants constants = new Constants(); + + /** Usual logger utility */ + private static final Logger logger = LoggerFactory.getLogger(SymbolsEditor.class); + + //~ Instance fields -------------------------------------------------------- + /** Related instance of symbols builder */ + private final SymbolsController symbolsController; + + /** Related sheet */ + private final Sheet sheet; + + /** BoundaryEditor companion */ + private final BoundaryEditor boundaryEditor; + + /** Evaluator to check for NOISE glyphs */ + private final ShapeEvaluator evaluator = GlyphNetwork.getInstance(); + + /** Related nest view */ + private final MyView view; + + /** Popup menu related to page selection */ + private PageMenu pageMenu; + + /** The entity used for display focus */ + private ShapeFocusBoard focus; + + //~ Constructors ----------------------------------------------------------- + //---------------// + // SymbolsEditor // + //---------------// + /** + * Create a view in the sheet assembly tabs, dedicated to the + * display and handling of glyphs. + * + * @param sheet the sheet whose glyphs are considered + * @param symbolsController the symbols controller for this sheet + */ + public SymbolsEditor (Sheet sheet, + SymbolsController symbolsController) + { + this.sheet = sheet; + this.symbolsController = symbolsController; + sheet.setBoundaryEditor(boundaryEditor = new BoundaryEditor(sheet)); + + Nest nest = symbolsController.getNest(); + + view = new MyView(nest); + view.setLocationService(sheet.getLocationService()); + + focus = new ShapeFocusBoard( + sheet, + symbolsController, + new ActionListener() + { + @Override + public void actionPerformed (ActionEvent e) + { + view.repaint(); + } + }, + false); + + pageMenu = new PageMenu( + sheet.getPage(), + new SymbolMenu(symbolsController, evaluator, focus)); + + BoardsPane boardsPane = new BoardsPane( + new PixelBoard(sheet), + new RunBoard(sheet.getHorizontalLag(), false), + new SectionBoard(sheet.getHorizontalLag(), false), + new RunBoard(sheet.getVerticalLag(), false), + new SectionBoard(sheet.getVerticalLag(), false), + new SymbolGlyphBoard(symbolsController, true, true), + focus, + new EvaluationBoard(sheet, symbolsController, true), + new ShapeBoard(sheet, symbolsController, false)); + + // Create a hosting pane for the view + ScrollView slv = new ScrollView(view); + sheet.getAssembly() + .addViewTab(Step.DATA_TAB, slv, boardsPane); + } + + //~ Methods ---------------------------------------------------------------- + //-----------------// + // addItemRenderer // + //-----------------// + /** + * Register an items renderer to render items. + * + * @param renderer the additional renderer + */ + public void addItemRenderer (ItemRenderer renderer) + { + view.addItemRenderer(renderer); + } + + //-----------// + // highLight // + //-----------// + /** + * Highlight the corresponding slot within the score display. + * + * @param slot the slot to highlight + */ + public void highLight (final Slot slot) + { + SwingUtilities.invokeLater( + new Runnable() + { + @Override + public void run () + { + view.highLight(slot); + } + }); + } + + //-----------// + // getSlotAt // + //-----------// + /** + * Retrieve the measure slot closest to the provided point. + * + * @param point the provided point + * @return the related slot, or null + */ + public Slot getSlotAt (Point point) + { + List systems = sheet.getSystems(); + + if (systems != null) { + SystemInfo systemInfo = sheet.getSystemOf(point); + + if (systemInfo != null) { + ScoreSystem system = systemInfo.getScoreSystem(); + + SystemPart part = system.getPartAt(point); + Measure measure = part.getMeasureAt(point); + + if (measure != null) { + return measure.getClosestSlot(point); + } + } + } + + return null; + } + + //----------------// + // propertyChange // + //----------------// + @Override + public void propertyChange (PropertyChangeEvent evt) + { + view.repaint(); + } + + //---------// + // refresh // + //---------// + /** + * Refresh the UI display (reset the model values of all spinners, + * update the colors of the glyphs). + */ + public void refresh () + { + view.refresh(); + } + + //~ Inner Classes ---------------------------------------------------------- + //-----------// + // Constants // + //-----------// + private static final class Constants + extends ConstantSet + { + //~ Instance fields ---------------------------------------------------- + + PixelCount measureMargin = new PixelCount( + 10, + "Number of pixels as margin when highlighting a measure"); + + } + + //--------// + // MyView // + //--------// + private final class MyView + extends NestView + { + //~ Instance fields ---------------------------------------------------- + + /** Currently highlighted slot, if any. */ + private Slot highlightedSlot; + + //~ Constructors ------------------------------------------------------- + private MyView (Nest nest) + { + super( + nest, + symbolsController, + Arrays.asList(sheet.getHorizontalLag(), sheet.getVerticalLag())); + setName("SymbolsEditor-MyView"); + + // Subscribe to all lags for SectionSet events + for (Lag lag : lags) { + lag.getSectionService() + .subscribeStrongly(SectionSetEvent.class, this); + } + } + + //~ Methods ------------------------------------------------------------ + // + //------------// + // pointAdded // + //------------// + @Override + public void pointAdded (Point pt, + MouseMovement movement) + { + // Cancel slot highlighting + highLight(null); + + super.pointAdded(pt, movement); + } + + //---------------// + // pointSelected // + //---------------// + @Override + public void pointSelected (Point pt, + MouseMovement movement) + { + // Cancel slot highlighting + highLight(null); + + super.pointSelected(pt, movement); + } + + //--------------// + // contextAdded // + //--------------// + @Override + public void contextAdded (Point pt, + MouseMovement movement) + { + if (!ViewParameters.getInstance().isSectionMode()) { + // Glyph mode + setFocusLocation(new Rectangle(pt), movement, CONTEXT_ADD); + + // Update highlighted slot if possible + if (movement != MouseMovement.RELEASING) { + highLight(getSlotAt(pt)); + } + } + + // Regardless of the selection mode (section or glyph) + // we let the user play with the current glyph if so desired. + Set glyphs = nest.getSelectedGlyphSet(); + + if (movement == MouseMovement.RELEASING) { + if ((glyphs != null) && !glyphs.isEmpty()) { + showPagePopup(pt); + } + } + } + + //-----------------// + // contextSelected // + //-----------------// + @Override + public void contextSelected (Point pt, + MouseMovement movement) + { + if (!ViewParameters.getInstance().isSectionMode()) { + // Glyph mode + setFocusLocation(new Rectangle(pt), movement, CONTEXT_INIT); + + // Update highlighted slot if possible + if (movement != MouseMovement.RELEASING) { + highLight(getSlotAt(pt)); + } + } + + if (movement == MouseMovement.RELEASING) { + showPagePopup(pt); + } + } + + //-----------// + // highLight // + //-----------// + /** + * Make the provided slot stand out. + * + * @param slot the current slot or null + */ + public void highLight (Slot slot) + { + this.highlightedSlot = slot; + + repaint(); // To erase previous highlight + + // // Make the measure visible + // // Safer + // if ((measure == null) || (slot == null)) { + // + // return; + // } + // + // ScoreSystem system = measure.getSystem(); + // Dimension dimension = system.getDimension(); + // Rectangle systemBox = new Rectangle(system.getTopLeft().x, + // system.getTopLeft().y, dimension.width, + // dimension.height + // + system.getLastPart().getLastStaff().getHeight()); + // + // // Make the measure rectangle visible + // Rectangle rect = measure.getBox(); + // int margin = constants.measureMargin.getValue(); + // // Actually, use the whole system height + // rect.y = systemBox.y; + // rect.height = systemBox.height; + // rect.grow(margin, margin); + // showFocusLocation(rect, false); + } + + //---------// + // onEvent // + //---------// + /** + * On reception of SECTION_SET information, we build a + * transient compound glyph which is then dispatched. + * Such glyph is always generated (a null glyph if the set is null or + * empty, a simple glyph if the set contains just one glyph, and a true + * compound glyph when the set contains several glyphs) + * + * @param event the notified event + */ + @Override + public void onEvent (UserEvent event) + { + try { + // Ignore RELEASING + if (event.movement == MouseMovement.RELEASING) { + return; + } + + // Default nest view behavior (locationEvent) + super.onEvent(event); + + if (event instanceof LocationEvent) { // Location => Boundary + handleEvent((LocationEvent) event); + } else if (event instanceof SectionSetEvent) { // SectionSet => Compound + handleEvent((SectionSetEvent) event); + } + } catch (Exception ex) { + logger.warn(getClass().getName() + " onEvent error", ex); + } + } + + //--------// + // render // + //--------// + @Override + public void render (Graphics2D g) + { + PaintingParameters painting = PaintingParameters.getInstance(); + + if (painting.isInputPainting()) { + // Should we draw the section borders? + final boolean drawBorders = ViewParameters.getInstance() + .isSectionMode(); + + // Stroke for borders + final Stroke oldStroke = UIUtil.setAbsoluteStroke(g, 1f); + + if (lags != null) { + for (Lag lag : lags) { + // Render all sections, using assigned colors + for (Section section : lag.getVertices()) { + Glyph glyph = section.getGlyph(); + + if (focus.isDisplayed(glyph)) { + section.render(g, drawBorders); + } + } + } + } + + // Restore stroke + g.setStroke(oldStroke); + } + + // Paint additional items, such as recognized items, etc... + renderItems(g); + } + + //---------// + // publish // + //---------// + protected void publish (NestEvent event) + { + nest.getGlyphService() + .publish(event); + } + + //-------------// + // renderItems // + //-------------// + @Override + protected void renderItems (Graphics2D g) + { + PaintingParameters painting = PaintingParameters.getInstance(); + + if (painting.isInputPainting()) { + // Render all sheet physical info known so far + sheet.getPage() + .accept( + new SheetPainter(g, boundaryEditor.isSessionOngoing())); + + // Normal display of selected items + super.renderItems(g); + } + + if (painting.isOutputPainting()) { + boolean mixed = painting.isInputPainting(); + + // Render the recognized score entities + PagePhysicalPainter painter = new PagePhysicalPainter( + g, + mixed ? Colors.MUSIC_SYMBOLS : Colors.MUSIC_ALONE, + mixed ? false : painting.isVoicePainting(), + false, + painting.isAnnotationPainting()); + sheet.getPage() + .accept(painter); + + // The slot being played, if any + if (highlightedSlot != null) { + painter.highlightSlot(highlightedSlot); + } + } + } + + //-------------// + // handleEvent // + //-------------// + /** + * Interest in LocationEvent => system boundary modification? + * + * @param locationEvent location event + */ + @SuppressWarnings("unchecked") + private void handleEvent (LocationEvent locationEvent) + { + super.onEvent(locationEvent); + + // Update system boundary? + if ((locationEvent.hint == LOCATION_INIT) + && boundaryEditor.isSessionOngoing()) { + Rectangle rect = locationEvent.getData(); + + if ((rect != null) && (rect.width == 0) && (rect.height == 0)) { + boundaryEditor.inspectBoundary(rect.getLocation()); + } + } + } + + //-------------// + // handleEvent // + //-------------// + /** + * Interest in SectionSetEvent => transient Glyph. + * + * @param sectionSetEvent + */ + @SuppressWarnings("unchecked") + private void handleEvent (SectionSetEvent sectionSetEvent) + { + if (!ViewParameters.getInstance() + .isSectionMode()) { + // Glyph selection mode + return; + } + + // Section selection mode + MouseMovement movement = sectionSetEvent.movement; + + if (sectionSetEvent.hint.isLocation()) { + // Collect section sets from all lags + List
allSections = new ArrayList<>(); + + for (Lag lag : lags) { + Set
selected = lag.getSelectedSectionSet(); + + if (selected != null) { + allSections.addAll(selected); + } + } + + try { + Glyph compound = null; + + if (!allSections.isEmpty()) { + SystemInfo system = sheet.getSystemOfSections( + allSections); + + if (system != null) { + compound = system.buildTransientGlyph(allSections); + } + } + + logger.debug("Editor. Publish glyph {}", compound); + publish( + new GlyphEvent( + this, + GLYPH_TRANSIENT, + movement, + compound)); + + if (compound != null) { + publish( + new GlyphSetEvent( + this, + GLYPH_TRANSIENT, + movement, + Glyphs.sortedSet(compound))); + } else { + publish( + new GlyphSetEvent( + this, + GLYPH_TRANSIENT, + movement, + null)); + } + } catch (IllegalArgumentException ex) { + // All sections do not belong to the same system + // No compound is allowed and displayed + logger.warn( + "Sections from different systems {}", + Sections.toString(allSections)); + } + } + } + + //---------------// + // showPagePopup // + //---------------// + private void showPagePopup (Point pt) + { + pageMenu.updateMenu(new Point(pt.x, pt.y)); + + JPopupMenu popup = pageMenu.getPopup(); + + popup.show( + this, + getZoom().scaled(pt.x) + 20, + getZoom().scaled(pt.y) + 30); + } + } +} diff --git a/src/main/omr/glyph/ui/UserEventSubscriber.java b/src/main/omr/glyph/ui/UserEventSubscriber.java new file mode 100644 index 0000000..af1adb0 --- /dev/null +++ b/src/main/omr/glyph/ui/UserEventSubscriber.java @@ -0,0 +1,26 @@ +//----------------------------------------------------------------------------// +// // +// U s e r E v e n t S u b s c r i b e r // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.glyph.ui; + +import omr.selection.UserEvent; + +import org.bushe.swing.event.EventSubscriber; + +/** + * Class {@code UserEventSubscriber} + * + * @author Hervé Bitteur + */ +public interface UserEventSubscriber + extends EventSubscriber +{ +} diff --git a/src/main/omr/glyph/ui/ViewParameters.java b/src/main/omr/glyph/ui/ViewParameters.java new file mode 100644 index 0000000..bf898cc --- /dev/null +++ b/src/main/omr/glyph/ui/ViewParameters.java @@ -0,0 +1,303 @@ +//----------------------------------------------------------------------------// +// // +// V i e w P a r a m e t e r s // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.glyph.ui; + +import omr.constant.Constant; +import omr.constant.ConstantSet; + +import org.jdesktop.application.AbstractBean; +import org.jdesktop.application.Action; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.awt.event.ActionEvent; + +/** + * Class {@code ViewParameters} handles parameters for SceneView, + * using properties referenced through their programmatic name to avoid + * typos. + * + * @author Hervé Bitteur + */ +public class ViewParameters + extends AbstractBean +{ + //~ Static fields/initializers --------------------------------------------- + + /** Usual logger utility */ + private static final Logger logger = LoggerFactory.getLogger( + ViewParameters.class); + + /** Specific application parameters */ + private static final Constants constants = new Constants(); + + /** Should the letter boxes be painted */ + public static final String LETTER_BOX_PAINTING = "letterBoxPainting"; + + /** Should the stick lines be painted */ + public static final String LINE_PAINTING = "linePainting"; + + /** Should the Sections selection be enabled */ + public static final String SECTION_MODE = "sectionMode"; + + /** Should the stick attachments be painted */ + public static final String ATTACHMENT_PAINTING = "attachmentPainting"; + + /** Should the translation links be painted */ + public static final String TRANSLATION_PAINTING = "translationPainting"; + + /** Should the sentence links be painted */ + public static final String SENTENCE_PAINTING = "sentencePainting"; + + //~ Instance fields -------------------------------------------------------- + /** Dynamic flag to remember if section mode is enabled */ + private boolean sectionMode = false; + + //~ Methods ---------------------------------------------------------------- + //-------------// + // getInstance // + //-------------// + public static ViewParameters getInstance () + { + return Holder.INSTANCE; + } + + //----------------------// + // isAttachmentPainting // + //----------------------// + public boolean isAttachmentPainting () + { + return constants.attachmentPainting.getValue(); + } + + //---------------------// + // isLetterBoxPainting // + //---------------------// + public boolean isLetterBoxPainting () + { + return constants.letterBoxPainting.getValue(); + } + + //----------------// + // isLinePainting // + //----------------// + public boolean isLinePainting () + { + return constants.linePainting.getValue(); + } + + //---------------// + // isSectionMode // + //---------------// + public boolean isSectionMode () + { + return sectionMode; + } + + //--------------------// + // isSentencePainting // + //--------------------// + public boolean isSentencePainting () + { + return constants.sentencePainting.getValue(); + } + + //-----------------------// + // isTranslationPainting // + //-----------------------// + public boolean isTranslationPainting () + { + return constants.translationPainting.getValue(); + } + + //-----------------------// + // setAttachmentPainting // + //-----------------------// + public void setAttachmentPainting (boolean value) + { + boolean oldValue = constants.attachmentPainting.getValue(); + constants.attachmentPainting.setValue(value); + firePropertyChange(ATTACHMENT_PAINTING, oldValue, value); + } + + //----------------------// + // setLetterBoxPainting // + //----------------------// + public void setLetterBoxPainting (boolean value) + { + boolean oldValue = constants.letterBoxPainting.getValue(); + constants.letterBoxPainting.setValue(value); + firePropertyChange(LETTER_BOX_PAINTING, oldValue, value); + } + + //-----------------// + // setLinePainting // + //-----------------// + public void setLinePainting (boolean value) + { + boolean oldValue = constants.linePainting.getValue(); + constants.linePainting.setValue(value); + firePropertyChange(LINE_PAINTING, oldValue, value); + } + + //----------------// + // setSectionMode // + //----------------// + public void setSectionMode (boolean value) + { + boolean oldValue = sectionMode; + sectionMode = value; + firePropertyChange(SECTION_MODE, oldValue, value); + } + + //---------------------// + // setSentencePainting // + //---------------------// + public void setSentencePainting (boolean value) + { + boolean oldValue = constants.sentencePainting.getValue(); + constants.sentencePainting.setValue(value); + firePropertyChange(SENTENCE_PAINTING, oldValue, value); + } + + //------------------------// + // setTranslationPainting // + //------------------------// + public void setTranslationPainting (boolean value) + { + boolean oldValue = constants.translationPainting.getValue(); + constants.translationPainting.setValue(value); + firePropertyChange(TRANSLATION_PAINTING, oldValue, value); + } + + //-------------------// + // toggleAttachments // + //-------------------// + /** + * Action that toggles the display of attachments in selected sticks + * + * @param e the event that triggered this action + */ + @Action(selectedProperty = ATTACHMENT_PAINTING) + public void toggleAttachments (ActionEvent e) + { + } + + //---------------// + // toggleLetters // + //---------------// + /** + * Action that toggles the display of letter boxes in selected glyphs + * + * @param e the event that triggered this action + */ + @Action(selectedProperty = LETTER_BOX_PAINTING) + public void toggleLetters (ActionEvent e) + { + } + + //-------------// + // toggleLines // + //-------------// + /** + * Action that toggles the display of mean line in selected sticks + * + * @param e the event that triggered this action + */ + @Action(selectedProperty = LINE_PAINTING) + public void toggleLines (ActionEvent e) + { + } + + //----------------// + // toggleSections // + //----------------// + /** + * Action that toggles the ability to select Sections (rather than Glyphs) + * + * @param e the event that triggered this action + */ + @Action(selectedProperty = SECTION_MODE) + public void toggleSections (ActionEvent e) + { + } + + //-----------------// + // toggleSentences // + //-----------------// + /** + * Action that toggles the display of sentences in selected glyphs + * + * @param e the event that triggered this action + */ + @Action(selectedProperty = SENTENCE_PAINTING) + public void toggleSentences (ActionEvent e) + { + } + + //--------------------// + // toggleTranslations // + //--------------------// + /** + * Action that toggles the display of translations in selected glyphs + * + * @param e the event that triggered this action + */ + @Action(selectedProperty = TRANSLATION_PAINTING) + public void toggleTranslations (ActionEvent e) + { + } + + //~ Inner Interfaces ------------------------------------------------------- + //--------// + // Holder // + //--------// + private static interface Holder + { + //~ Static fields/initializers ----------------------------------------- + + public static final ViewParameters INSTANCE = new ViewParameters(); + + } + + //~ Inner Classes ---------------------------------------------------------- + //-----------// + // Constants // + //-----------// + private static final class Constants + extends ConstantSet + { + //~ Instance fields ---------------------------------------------------- + + final Constant.Boolean letterBoxPainting = new Constant.Boolean( + true, + "Should the letter boxes be painted"); + + final Constant.Boolean linePainting = new Constant.Boolean( + false, + "Should the stick lines be painted"); + + final Constant.Boolean attachmentPainting = new Constant.Boolean( + false, + "Should the staff & glyph attachments be painted"); + + final Constant.Boolean translationPainting = new Constant.Boolean( + true, + "Should the glyph translations links be painted"); + + final Constant.Boolean sentencePainting = new Constant.Boolean( + true, + "Should the sentence words links be painted"); + + } +} diff --git a/src/main/omr/glyph/ui/doc-files/GlyphsController.uxf b/src/main/omr/glyph/ui/doc-files/GlyphsController.uxf new file mode 100644 index 0000000..2648570 --- /dev/null +++ b/src/main/omr/glyph/ui/doc-files/GlyphsController.uxf @@ -0,0 +1,57 @@ + +fontsize=14 + + // My own comments +com.umlet.element.base.Class600610170150SymbolsModel +-- +assignGlyph +assignRational +assignText +cancelStems +deassignGlyph +fixLargeSlurs +segmentGlyphscom.umlet.element.base.Class180610250120SymbolsController +(for most symbols) +-- ++asyncAssignRationals:RationalTask ++asyncAssignTexts:TextTask ++asyncFixLargeSlurs:SlurTask ++asyncSegment:SegmentTaskcom.umlet.element.base.Relation440150180330lt=<-160;310;20;310;20;20com.umlet.element.base.Relation3908029040lt=.270;20;20;20com.umlet.element.base.Note66040120100 +_Data side_ +- Permanency +com.umlet.element.base.Note23040180100_UI side_ +- Build Task in Script +- Act asynchronously +- Publish on event bus +- Update following stepscom.umlet.element.base.Relation40015028040lt=<-260;20;20;20com.umlet.element.base.Class200160220140GlyphsController +-- +locationService +-- ++asyncAssignGlyphs:AssignTask + ++asyncDeassignGlyphs +-- +#publish(Glyph) +#publish(Glyphs) +#syncAssign(AssignTask)com.umlet.element.base.Class28049027070BarsController +(for Barlines) +-- ++asyncModifyBoundaries:BoundaryTaskcom.umlet.element.base.Class660160120200GlyphsModel +-- +lag +sheet +relatedStep +latestShape +-- +getGlyphById +assignGlyph +assignGlyphs +assignSections +deassignGlyph +deassignGlyphs +com.umlet.element.base.Class60040011030LinesBuildercom.umlet.element.base.Relation44039018040lt=<-160;20;20;20com.umlet.element.base.Class13047013040VerticalsController +(for Stems)com.umlet.element.base.Class12036012040BasicController +(deassign=delete)com.umlet.element.base.Class60045013030HorizontalsBuildercom.umlet.element.base.Class60050015030SystemsBuildercom.umlet.element.base.Relation72034040180lt=<<-20;20;20;160com.umlet.element.base.Relation70034040130lt=<<-20;20;20;110com.umlet.element.base.Relation6803404080lt=<<-20;20;20;60com.umlet.element.base.Relation27028040230lt=<<-20;20;20;210com.umlet.element.base.Relation25028040350lt=<<-20;20;20;330com.umlet.element.base.Relation23028040210lt=<<-20;20;20;190com.umlet.element.base.Relation74034040290lt=<<-20;20;20;270com.umlet.element.base.Relation21028040100lt=<<-20;20;20;80com.umlet.element.base.Relation41060021040lt=<-190;20;20;20com.umlet.element.base.Relation5304909040lt=<-70;20;20;20com.umlet.element.custom.Text430023030_GlyphsController & GlyphsModel_com.umlet.element.base.Relation5408040740lt=.20;720;20;20com.umlet.element.base.Actor206084120User Gesture +com.umlet.element.base.Class201808030GlyphMenucom.umlet.element.base.Class202208030BarMenucom.umlet.element.base.Class202608030GlyphBoardcom.umlet.element.base.Relation8017014060lt=<-120;40;20;20com.umlet.element.base.Relation8019014060lt=<-120;20;20;40com.umlet.element.base.Relation8021014080lt=<-120;20;20;60com.umlet.element.base.Relation8021014040lt=<-120;20;20;20com.umlet.element.base.Relation8017014080lt=<-120;60;20;20com.umlet.element.base.Relation40026028060lt=<-260;40;20;20com.umlet.element.base.Relation400260280110lt=<- + +260;90;20;20com.umlet.element.base.Relation400170280110lt=<-260;20;20;90 \ No newline at end of file diff --git a/src/main/omr/glyph/ui/doc-files/SymbolGlyphBoard.png b/src/main/omr/glyph/ui/doc-files/SymbolGlyphBoard.png new file mode 100644 index 0000000000000000000000000000000000000000..0bd3a0665cca4740b2a249bf643f26b6bb4f976c GIT binary patch literal 59884 zcmV*&KsUdMP)9xQR6%%5}(Ex z=R}`DQN&1)Xv|=u7{#bTjYi{;#HgS+LIgt4r^W;nWRRxm`A%ov^RBA({@CZ-dv14Q z)PU&svA*-|d+t89_pVyI*1T#JYcKsb$cAwN7@h$fdIC_8Eigr<2I3GQU|<49LKwq9 z#B-2@t^pvCfMsAB0LTJU;4xsB4gf*mhJnB}12Gr|VZZ=Rh5^EqCyizxW&j8y86Jcq z11G}?iX)bi7;_8hC|Jg5ZX#P3LfIO&Ax8-V9$7*&E)5sRzy|=qR6q`l16Dxv!E$a~ zkOSia#CV^04ss0m13l1HFdP~HAV3n}AO=Q&0aGY{Vg@7u2I7XnvaEjCt5)lIo-+sl zMCF7s!^Y(o5SRhLAXgv8Kx-{W9RP+i69B*g4&rR=S5D=%YrtrTKw?JvZgn z=ecEBj4`DY0}+wu1)TFN%K!u7U>FX$oMsGbxT13OVuZ$3Se@h?#Mfw+jUjpb3R)#h z9>=lg2A=H|g-X*jj#DyBO34@_A|eJ5V+?>a-~byV>Jh(T1~80h?ZE&*#1xnYi~%?= z3^*8p0bs<+{&K(u0J1qTKr@)~_G=vk`0%13O^&V|_2EIdci=D_#*i30N2>&87z{)C z+BnRR_pN~$Z#;fE{gP@U_s&y}1X-3jo=rrq>q=oKNd~YuPHfx8xX>G6fDBuE#443y zoMD-Dh+%w0&qFBjc4Xu+BZUCt{r*5Hu}f1AZxi8Gr!B2%Hgz zDRq9szzB?}l!vRFbBwvK@^u=$+_kB;P6_%YhePDmIvt7wn4FxYi(IJ z=Ugc@YBsKg^0gDIH(65y%OAx0_J_{bvPb$bEsqh1Gi^vPMn?Fa@B5V~isCpfilSL> zXiY?la|U7rEkR;E<1r;E^0FVL$R+?B1j9f~jr1Qg>~txz*KWdTbWg*KBm-tRxPbvU z8_7d9)KN{Tu$OwQ8D~om!jYhSD;x#}y_7Xx%+HV3;AD3*kr!A*?t~;y9^RYp(06yf7v$2f-L4f&`7s)-~3sz-T0Q&4|)4 z;;g)U%>cl10DuI^@(Kn;kbnhnK&|4dR6jX}gg2yZU{$#tjp{Hh{Xtj0Kgbh!cRQKX6@_h!|t_dUI)M z;ZNW8Hl>u-y6ikr>3SMIcvXkiD5;Z6)>oqK5KQK`HV6M+Q+g;6hjo+_62SRGMa$TGJFdjU&#)p;wgS zG{e!ZIt~vLazm-ehy#Y0F{_&Vx?ZuhInCAtZxki9S}lvSJkR4eo|>A5s|@zpCf;W z0OWZp8Rv$wII$d?3sGn-zyz4e3$1mMrMBadBFP1eF~-=opC(E$X_VrORFMg8se+W! zj4{SY2%(g59Pd{nlEjQLMhKB*nd`bmp{1~l0U0KQ;GCBeiE|FX7-QSE5Tg94j3O9g zv@%-jG>wV0WeFk;W(t)RMMk6Vtu4!1jc=NfGlm1k zK&XOiEe&IFlub@dXL+J1lXjpAzeshxIg@2wrN9daK`0qwc6?~hgmo~ zw=qraIMJM#<2R!uvVBYGyxnRAmgQMC2lK0yLMtvsR%q9CN&o72^M3KbNU3~kVzS=} zVfe(v)F6o!hzV&4&Osz?-}lMzG)@P-cy4AUQ<>}fQJ6ZmPliguytRM&&V~*o=I=%V1%~6^dqgOo-hun)s0DxeK zz=#_4YPZu;xu{M}?!9Nv^xSls=F;L(6a}8|G-^p0C2^R9(d6VL=V0MGnVS zN@5ZK4C6#LnV?c_XcTeQUT6)3M4Wb~CN^Js>BsxM=*2I2`Z33Btol>k&Vpb?P^o6A z_Pwg(R&Cejb`sPZs-Q4sK=97@Ub@m*IsXmkZ8>UVuA(9@<~ME2!%zTu99e`YN~C3F zN_&AzbZ8*#L0AJKttd${rU(p0Q54!34o^B^*sBM=&ecGLg~DWWYGPvZft@@3hM%UH z(VSV9QfZ~)54x@9)O6S{Ea^s3W;?DjD$A7+_U297!rscS1S_o7YR0Id&?_qobF*8j zRgom=ARGjhvFsT(^oRhOzzDSZ0Sp);qw^%TIM0hb?e`{{b=P& z&CKMKB%(sM`W?UFdKIs*t-R1H3kw_OX0jx;1)rFk-MeSc%>2y0{d*=Sn`s(z0pGXD z=#mDBG{Cer#;7Awjf z&;Em{_HwFDZrHlqOZFTH4R6@hsrDetjn#`#m_~u-zJ*?tL->X{9+)Up%m%hzVf{F14ast>(DC zu$*o$#4jlvBH}2H!-a(f+qSoE-P-T>wWdFM(?wAf4^tb!8Dm4&)S;k_QN}1^a)R#n zx@SM;tYeSa(Wq~nXl(rIm%cPRJ>6_Jby0YZGdnXsJu$6vU1Vy|>(%@KO1GAlTT9Ef zWdTT{cxiF5QmgfQgIcYYB}uhfbsUE=7KWi^3xY8wH%67u)mj^KXkuMCI?2g^DKkt3 zl7Jv)3^||q=P&Qs*Z)5+c=;nA{qRDgQnOmUyIp^S+t_&1oi}wdH8V9U4U>TMO+M&J ztIkndY00?&6LZ@$-R}&fSDB7ugqhVEnA|uOw?a}EEDbz6O$Wk~%(9dL6IN*`hGAF< zf}u~0;lbl!M;>QMzti75wdqIS`|pQ7^x;q5vhl|6e)qZ0dKQZk$}-z>@4S2GiKm^> zUhGu;iY*=2_4y#a=9=re?G}TWo1Q7Muu`cE!g#}m*mX+tZ(Gh3j6HY#9 z-@yZqeAJomeD~ix{s~WTY&+~nOz|X&E$~LOxqRTDHNPS2>~GfVeHujl!PM;hpw*db zPVKvAuWh>%Q_XI-EhOuAx@i_~+_Y)m-rcraF$6|1q=<+N)4EuT$&W#qwl)+_2CR%U z60AbYPAA*AWrv0X>2>;XP;W5DJDIlUz2b&H_`?sp;mv>khKv95l0W;v4{z)CV>G6>Buakllb^ltkKcCT#ee$MuYWVw zT-v@ij0vYGlx5l4z^ha%^%@yQhLxRu@ObEv*yP02|so7K6+cNKGW}q=REESQo3iKefHH?Uu}$8Jh=3t z-+9rs*IsAa?(1Lw`k9&Ik3asz+kSLMr8e=4A*ufb3}^7(y?b5HmQuD_ty-;i^UXIO zcicm|o&K2o98%*dDGr4pDS{?4M228sD2+2mI<1xVhPjO&zWk%_d(Zo_B!B1`zg7>L z0@9Yw*=L<|;GX@@dd72J_{`_b&g{7I3txQKJKsGwyW^4z-`wf+9)HdgFZ-MK9kXq_ z@78bs(H%yEh)*=9_V3$g+wMpR7eiN(&JRsC7&C3hBc75V3eHU^geP2g{SA)ec%JvZ%PxEAOJ921ZMWb0qaPi8%#Mm*-F4f} z{d?}-w(aOBiWe7_Uj6FV+I=aC&0u&O3jcrdb>%^Ya^9t&Y)n{_}t5hc|uuHLv=^GfsQhk8b(? z7q9%v@$QG)j&iTD!3 zbs!RuAkKzPR9>jW^{ig6=T_@~`~D9fT-mG9x!_IjV9Z)s*{@A}(y7OO|Hl3QcHOtz z{f!3AvmXCcuZHE`AT#h9jj1_H`V;NtygvX0m(muD&rdrQzt)TUQaS{Yu9)AggoJ1&W@H0<+_)4!cKR@3q(wT|o)nEDAvmXD%Z(M!N zr~mn%q_mq}t-ZKnI9utqo54ib>v7Iz=cXR>n8&>NqSr@pe%Dfe!IQm_)eu>>-Kw$lVOOnq*AE>sMQ+#_8zF!8fmJ| zIPDClBIaghPCntpX0=Af1fK6W&N*j4-m;uiPdc^T>Mkvm_#fBqbShisH*Jd)s*~Q*AQGPLMp_$DtxO6ADl#fkp*3=0YY5`d z$aCcd^)O8=OHR*Dzxvg$bZiR{fE*>@-cPWO75&7Pe8jpAiFrSO))eU^5w3I5`4@C@ z9CO?$1_-LS=Br$t(cUH6?Z z)hC^DVv?n3wD$;rtWs5r_*b>6$e7~qxOO2 zgUqI&W)o9!5pS$+T0XE?tyj3T4Y5L*y6xt1RB>IKGYPVUFiI<>27|%G#6-W}uh!}- z?Jfj2iUBajaL!esvMdwa0*uy*jajm*h?K)zvrz^dgCS-(sm+@kL6z;n|M08PAbY_JpFc6hvlI)9yC<8qI2wSdUUzx2I$y1z z>KWw56WU?I;l}0Qrf`JMpvgcfrB#+DT5{rC>!0?tXAJsLmSs~j zldWDWXt-h6-LP@yz`=VqZ$9b;FZ`WzfBVI6yx`SLaL=<|^r9F3`?cSE-t(S6==R?K z{trI$nNQ!mc{A8}g4n9__9qhCFc@}!U}p8vBRXUTK&4V?wK`H-Q&SUr?l~Z(o#&Yl zLhFJFSzfmqKrbytA_D25*+`lhGB9ZzPt8shI<-Cf2e;qy_$NQ-gCBS|aTDiJFYGKW zE$q5y*DbgGsMT57e(VmVR2YVpN~IEb{eC}560KFGQn7722!fA(^do1Uc_w2l2!ex4 z9mlbfG%uCsGQ)=Kucw z(_IdoFPpq8sf(gsBr7KGbDaqIj1&=JxVi^glZFz)Df45;uz%kk4~KjCl?a>|v9T=v z2i}eweEZusUVi!IkABo+Ui5pvf7F(_&wuWU z&-~M;p7N9@@7=d+=iPVi+O>0XYQpoJ&097#>ekbr@w8g4vj4!oXFvOy@4EDze|X*> z0r=yKUU&5NZADQG*UJ6$%J~Da&aIR}olbY}o;|MXKj}$N+r4}D8E2ev>zzL!Dl-`v zV+`f=*BZJMIi;4Uhie55DWmu9-Fy1!kI4-D09Rjq#qr0Tph1^jdg)^y`_!r{&iu7A zk3Zo!+m?6SaVrFJtu)czefzvhb!KLYbN=HW|M=unPt<_L#SP!OQCec9wKP3B)$O*E zJhm(WrpwH4Vyx`Up_B*!V9U!t1Tb2gECFG_2~t`vmu{*|VVHsN1HauH*ut*5PHUxQ z``WQ;-}v$^U;D;&k9+(Xk9hPk8@7l}S6}vyZ}qzBwQu~b+5|I(Y$05(T}Ng~oaITy z^DQZgEHbL#i~tf$b4Gkj@i4=M%J-jC{0YY+A!uDv@2E|=VC5Ho1O#Jo99otY1m4QZ zN~KcqJWp%QIgg@*hy{M7C^TcN-w!>{38P-M8d#Q76q)P#oldJ>Z%WDg{cvXE{J}l@ zj8Va&~t;h`3T))%l1l1}yOY>}yh77{11Vl^< zOF{&0aA5DjiN-{pyRo4#%Hm6h&>`AvgCys)(L@>l%9WtUyHb=!8ObI133y{@KQ zL(fi6b^4v6$Ou~Jx+n_HWKn3X%~!wr)r&9rllIcmFqN3Jlv0i@$XK;`_ECDU`WVc73S{WMKo*X2T5QgjAw$Myz;!PL|=k(sJYXol3=rfM4}=u3ON*_C zoObf=yMOFgTvjd|8v!S7jL`;VAsK?EQe`FLK)c)L5P7aFN5*lyVdJJ=ub1VyZM$JK zu$kPyXYZCxn_;wN^OeqGP;+<5DW6lr3^0 zEN*f}Ib)2WTr>dAISd&_Qt;90%wPQR-iY&%*<&7vz76}(ZnveBS(X@MqG(V$f$LWrfMrDn64Wy<$`V~p$C zd7cU}zG(e_q_m$!kG&6I)ro-l`S~zKk~5;2SJeMd9@a_I-No#O68vC9(&v|yLa!lT}vA?G7;}pJH@D=#`s{%wQV}^ zY|FKXR5%!L$~_yFrAbqFWzl!sdfgAhxKi_zq@|L4$e@p9mXseV~laeIXE#<6xrd42)`&8)xcUi;@-=jJs_Rr zuIrYh=(=uE6prIW(IAQyz5y6y)D2`@lXVWw-wBnLmjy(t?-}jRw zaa}ixLS3ks$aj3tYxcvwTMd?4D^ZlpOwVy9qBNPAn+wCx_x;^__wLxSqu1|1D-JB| zKM({#nq|ruqe*LG%qXQnoXp|qHS)-=2xb^#c^YX&71y>T2QUcxf$L4w8;)%+EbOV& zf_{IHrMgj{j|S;PZ9^D!n^SE6{@bUfrt>teRBF1&UCYk11b`Gm+JXx(GMsZExW>@0 zMP|)eX9@Nb+3@`bt7+)tM?65CxmU>Zl*mcbv}9pn7*0$~cDrq3Os!s3g(jHhL_=%s z*zU^8l4V(wlat+U*RrglC;%MTx36BWYpsP4rI6&DZ`!meilQJ0ilX>mxMA*3AxV6k zM@uPZW@fXrAkx)p(C)OFjfw7{&n0Ju0etATAu366RAG&XFpBLFn6X`NrQON0#H$2a z7mnjF@I22w*Kf5~rY9ziF@o_yJZLtXahM2_jPYu%(GLf@(45N>lY_xvVzRNcyr6Xv zcy75$D@hVVq>VP(7-JYiS$1)q2X(lH9&Bat_OaoPhPt+T^bpK)ZY%)c30MJ^8?s?) zFdj&NXAo_$f?&b8AeRy67?*Nn7&r|p7aZ!)W#9x$Sc5?%rIb?Iwym{RN?DdQRwn3w z1)l8;1_Rf1vn;FE>q(NNSsVmGnT|wb@iE2-;Dv_oSK>G}##AbPr_LV)oQEN@;q;LW`1E|!LqFJ zHml!Nu079FN-<7FQ3xTl*41h?P2;h<{`|Hg9}n>#t}fB4_Rh;j#7t2X05`^b?q9E1 zUS4)xpD{*j{Kc%gnucWuS8rDSJp7ddro1~F8!xTaFC;b|fHlT2#?mD5JjZpNe!uTn z7862~A5yX`oRgk%;MS{H0(;jT3w-5S=)kq`SeR=$CQ zEW^N&8J>d`0Ee!?1RxJAgIom9h%Ar?}z_12CoWS%$(&-|XVz z;{5!4p67?R1mIY1DQtb;kK@=6+ye&=OiWB@U5vH;1(4$K$RuN&m*EUcx_-3^aFu1! z9-BX7g%uBrVT~77G;7rlXjIGOaCK=&B?hZzw$@+(=TsDI$if%^Q)q5kFecA3!Fd?= zvn&%5B`Hc?#?y(REJw>gYmYOGP?jEAoou>H94CuN05)L23$O&*0T^I`49EhBk?UEQ zGJA#$h9!AcDc=BN+zJNZoJlZ|<%+9erDEm=X+W-NYgTO%ckX~5^9)m2XNq|Fi!7@OC1W2;ODmxORvJw`` zdMcxArco_)?#OD9o2WksYE_*VObW?(VYDNyRps{QJ_5*d#lf;X@qOR6?Klpllxdo{ zt~(s^n1~}{WVlkw_x&&&OiWC)+wGz#9M@Wl1Fh2_?r&1$g)+A7Dy6hGjYcC&3n8pB zA#)thz*yPEp9P9SbI#*9u2d?e7iV&EvfWvcQUdF|@TCHuC0VUjlTs#0QWOP|@jS0T z=sJ!wMn(S{srXa1TK#6ZPL^e480VaEi-`LDm~-xX0oEyEP<5-rXGDA^9;6n+tR!7J{ zT=JH;ZQQgaOVdiV8b{GkO=PUd^TYj{KkE@d;06G)EVFHU|Ngxb6BCt6r6{sI&lx8G zC6@w9)1C!hwJ3^8rE>7#{tGU+KnUS^Uael~bUN!^>R4^|!)MBU2)^$}QF8g^A8OPm zdcA>CrdF+YyIsfe036B}R?qyjz+J~HNihh5C=R8RVb~7>uT&*@FiUoPYlL)oL}*^Fy_UpX8YRX}fnX<68TkB#{u7w5-8kaQWpQsnu$QLakP7 zcYCXCXf2%!TimXjOv%eRaTuN^xgjN`R4Oks z4c7Uz?i;wbc;;d59F1B~$qF*YEG;b=V>sYdD&x{@yllV0cJqf5FNwKHipgCVT}Y(`dy7-wgD5w0mGmPTr-QnC20V)M%}ht zrIjVPB_u`(&_}Ww%t&$I7_vG8XofWdb(FJc0A3bfwQM^aM3rg);=b>fTMk80xN2*S zR(zwhwuNQ2S}g#c?*J&x?kI|Wl1G<#S>sEPDtk#7rVw!|UB{f$P_r1hYYhM~be@~F z2rx+iR8|}&4j@y7GP+W!fw(cmxy6QMY)8sfdB@cWMq}FfYPK(1Ju}24P;Qq3QeYgU z!2|~Vuv+)0r>BY5MUu*x92o{sk>^~1L&kBOB+2CDw3Jp+OKvhPc*@IQMl?Q-y1|GCV6>88I;^G!x7V%phx}wwv04W-wIbd9DbS;8_}jG)Oa5 zfD8!6KoOF>D5)z>LO`QXP*6#^2Y`-}Wk5q~5Mamv8c@(0X3$EYVH7I0KpPE)D2|!n zS}V_Yz!Z^YMsMGC)X`hFGSY%EE}(Vc`VKKu=-lz_TxFU}P+ik@%K238?!*a>?Pobb zc+&QYG-=d4Lq$%7VnQ(?Y%5Gc=>;V1BF!X&<@x=B^{wCBCdt3TB2%<0n3@>3glLUtkB@LF$|gwg+W131c)%o41|JE5C#?* z2@uk>v6<+RA7tWu;m#O9V;IkQX2m z)M_eCTPrJ^BhQs>TLeT|5_ld81xOT-5`!+HEUkF;p|BwZMw$?1d17LqsMfuFFzmVz z0x+)Ymg80`XN+lGoOseh7-Pm@`}Sj#B;lNgN$duGwH9ctmCjw)WelxWYaPp9+fa=s zsdBua$nqo_xPGv-+{*I2Qt^dgQ4(t|CucScdL5&SN~MxzS-0D5Oi$WXp9^7Bv1!Bn z^73+Fv{wmQ-A?m*%l*0C4f>e8kQ6o2Cumvc$Ot5CnpDk!LR@NuX|oF7^GXa z9BmjawUz)(PtPtcE_$w?rule;YHSXzDlhA1RXNQjn^Sk(aff5O?d6v1`V1n^mF>7e zwKj;tyvVj~-@3B6B!y6!G6s(07?s<$)oyhk{iw5BD}%5<2!f#9=_sRxw79TxrN9KZ zgwg;YC4>ZcrQ&x7S!OWg)J7vyupGn@auO183)cTq~L~%g9rC}uKm!{Pj_uQj`jBKCpeCkB$4NNX_|IAooclzWmz71xZ7!& zn|}X7nY}STKf8a|T@w>09)JAfkD5F2w%cw4Fg-Kl`IW`xR{-+=>$L2I3+snTXJ z7zo>jWdT^+yLao>t@HEq6BARn-g@hDtHl7%@v=PA+C1WfGb(}aIPQjx^=;cu=ydg- zAMc)THauZTF0TCIwVq@9+>ZL~SH0qOag=ib07{uW&yyr6R8cN(vn=b3GtQ7w5>cL` zDDaSnoTjz*eLqdp!C+7(F_sx;)6>&O9xV7tr$79^Zo0{3*$=+|?ddJYroyS$rj`yI z)J1o0zPZ$kckWx7snIMZq+r*w)-~{M<2qAZ3LgecEX^{b1*= zTlPNYl+!8>0ZiI$z4GO+U0&8wfODj&GR6pQ0leGoW?6CY;K8Rp^{Hi& zL$%@o*tqey`T2R%(8b`C!b8(mjK}`xz zDZ`~@${h$wMyYea9N2w#z2aMzH$6SQ>#lu@0Kk$0mWQ-B_2iSifOF|KCnjpOO@ceE zR_CbMY0t8zXPeht^IuW|U<~ZJzxlF#`(gmyZpXIlO2sEYN?zurOixdrb=Fy794A@k zx=bl-+qSLDnkW|;_xt^l5HT`{aOld6`yNc5XHpbt8rEu+TW-1fVUK#^*KYjU=B>xI zSGtlsRpj#5?kq~K1q3Y~|YUZq~W@WKoC?%m5Q@0-_M|Hwx^GRxvl zx7Tb`ZQCxlXAIL=#ToPL++kS~Kks?x{`)uoZO2K+^+R>aqn`QjM_pI^Y87D$jGRb$Qgp(lj;3_`dIXUccWjid;%5 z1v~P9AsxSV)6M_&`Zv7xjvxPkODj%k%jTnZANWx)5m>IXaemA4-rH-QrHic7>w49i zW!c>@nrSwzZYV)2FZzRs3Eu7xYL*2l7MGW{Y}(#mI&fq!G6TS0^{Q9hwd-!qYd^Ye z*TWw1sO9#u7a%FLut;cZ*)acw&;9Gf@f((Vy`V8;6P|eb*&-Et>;O!*#0F%JRWwQEA$)fI!sGp7N9@ef{fKAAjPB`xm-9 zPJZ-(#d}Iai~Co;e)TsR)yaOp&p3IW6NX`@ z)0vywKt!cdYK$3qXv=`z{lAGf&FQ)0Hf%a;!-hv-5Na~r zelJ%==@thN$8lw10$P9kQ=ei|0yyQAQy%%qNA5dtpxLOGL+E$gh9{~skd7lbD|BJ` zlx01x3A|DBr%rw7!~BZf463)^{KL~uKBL;3L4U-iXre4}2ID3Z;ak303Dk2&G=a~jnh zm%sO2t%C;*bB{mip&RB;JnPIy+;!JoNs>J48BhKG_rBYSlG&q<`}ViLohH$#r=I-7 zTW>k~AtycYsn0z2sN(|HebXObOu{;HWRBsVKL2@XyG2o)c;c}qoN&U*N*jP@2^;QU z5Nkx$2^%D)8Z$aayKPpw2m47^RvLLmPkZvyCL6ZW`E~z(!`z0Aje3x#U<{n2D70lc zt{V&nkt+1Tg9~Yz&dtr0nS;)Vb5$x8BJw=X^Spxx50-wyBZ4Wn9~+Bc6EnojjW^z~ zZQD_^Gt)^NO*HD=*2-@@{+!+S?Df4ymg{rQIs3*NZ#?y+hyK-HzGw5+tyg{NOK-mL zja!d8^$Y)c6|@X|_pO&)ym|hp9otU1{r~`OIfM^KrfIt#|JH78=!l6mC9hTbdVc-~(7# zSn#V=og}bra1O1F)>mJB^?B!=H#<8EAXj-5N3~{?b3QRY(Fp3Br?$7d9Rd3rU%UEA zPkeI2VH3^i%m4l|X#<8_$Frm@EC5)|EH9Z;8J6Xod!Co)x$C-) z=iR;k)@!c+;#FV!`j76u$1uKT!w)@hd?aGH<2e8N`7gfd!V9-f&8nhM}eZ+g?Ir#5@w>Ipd5o zzJ232KKyqdQyC8I+w+`fJ?oC!est9rue|*4KC-mZ^@0i!%hfR8q>4-{g)D(I~@i%junQX)_QJkuH09+R;%@+FiDbXwOS@Hm&|M9#*O6+Tvbq4;r>=o zQLP1OqI&&K;MsYegM)J($Kl<(cE0cTpSy{++-Fx$A6-Ch<^#A3GD~>zy4<{@3owx0sncZ~OIZx&`+q89d z`?h+TMBn+|%@_Ugrv*1pdFqp1chP$sKiE7~wM_EYfA{HEzTu)zfA}vui~Ek+FrDYQ zyW!}^KjXQ9jp>>k#Ia%)fv>B4H40i3X=ZuWRaaeg#~pXR@q#x3s8q@z6NF{`*R4At z013iS%-uFE2sb_xiYu`HWHE+J+ipu~PUO0|j6aZlmqFRIR z+i99!d+l|Dup5SbM_NluOXU_$j^k-v6qI&4o%xv!{a#!YI*HR>uXhBztcSJm%1BLp z#xtIA@W7M5a@ChP=kNKOcVBS91uuK)OaAh&-}M_${jE2=>5Z>^<;xt$nVg(?(r-My z+3-2%r=51%)}xQ{z3RaO%bPZAe8j`fs8y;bo^bLlKfFE9G#L)!ObP-gXl`-tI532w zfaQMV^1okdh3B3BS^#ktdqO+hW4`x;JMI8DT<`lg-FEJCU-;E8UHQ&;z4tSp`P8qU z`D>r~{8ujf+xK63=|zs?n>bC9WNxDu4x&c02{5IM*7{3d`cgmI7bhLhsdhU9+wukH zgKmexL{U_$)s~lAX_{`@v`I?ox^5YrE(^3WcK?%Nk|cR%>h(Go%yqpY2N4M&(lix9 zSeD~40i#^kCL$@N@B3+*xsplAm~DfMQtBlye(@=%Km7G?ybypgrdF*CU6+EFxiK*5 zqkrR^TVDHSF5GLbx#pr*zYHdO;Y(ifmiPbm@(OAbK(7s;UITzU&t2CBFgG`M=9y=n zbN1sHUriq=D=a}5xv;F&U|kaRKlGswRjXCYvb0udn%;}8$Z}<_xbk1#{oX(O!skDG z?laEbxM@qf+i%qBdA1k?-r`a(Nfjr<$%J9(xcgMo&3nRTyil8VPw1t zkF&&aJe6k-b3h8Lc=Tf*8L9U!wQ$4rH@x+tKj2cI^MteC^e3P13^3tDHYXT07-aQ^ z<2V3N6xh6Z^Ev07ei#$3U zJBPq(gwZf``@Q+uxzZ=}j(7a^dFP#X^if;mIKKAU>+ALU6Q1yd&QiPEk6qXAw0q5F zb82ep;J*EySDkD&iy}|rP)to1X*4-GT`Dz1 zU#}Um48G$FWFX|dz8&}wmN7W)xZ_%_lsR`DC)oLrN2i>gu=P$#+#4caV(hHu&MgJ z|Na(O<@-0@>bZ440C*pMQ4}gmfng*A<&F!EqywqA=>i7Fg z3k&=A?z!u(AMf0G*TTZW?%lh~mE)zRdc?sR!14mm^WOFLi{JOIzvw1v@&A+e=Fyg2 zb)EQkPiH=Ndh>fVB$bds0>~T)1`xE>pN$CGK_Ey(TN%UvcGF0Zrk4;P3q*u~4KlR4 z6no+CXSaybM1{zd5THUxRg#)ty?VoacRKSvd;0yc?|b#C>b;O6DTDTRopsi|=bpRI zIp4Ftd++b``6fK{U0oy*MRip;&5jpOJpPJ(pZ}{ruWR}DxBZh+dVX&1vP&-g(m&qy ztH1K9`PsSGzy9@~``mZaY-EfAP&T~{8Ne9TbuDWV1>ty&Ju_P4+F zt#1W@x(46JM~~&YtG(C9Vfo`{Z#x85#V`JaQ>RvJ z+K1FiBOrt%NfI_<3oL>$Hb_@h@elua+vS%(#`9f-z;PVACiJ|Zsw>PW0NnDfcinW; zO|SdS*8l>efDkx(?9?w__mU!8#x(!!-+YZO6gIGb|9!2AAZ(i1`SzW6+y&CV>(1}Z z%}+)RP<6!^i{m)YOXiJv#26ujC}oD}kSpHr_XndvRaJ|Ni%UyO*>E`O^$s39xW2M- z;>eMM_uQkDN|MBy12^@=3qC7mSr$bN*JXqL=<$!c#<=kO=l$I0zwp=Zd*3a;^up`^ z;FgbH_M^MQxe39mR@~?iR{+Cuiqw|H_wTEilHE z(74ff?dyI$Xh(Y*3qw)e_Q1hrNI&tZPd@3A%huDEJHf)=-*b{u(7e9-@jrV1adOF} zkN@s{UzwN*v<8fUbB+=7a zw@h$eX)UKZ&5wNWgHL+m^Y6Ort|Cu-kBo-HnaTP7a^DZbKl$`0pZbI=K78xP(;EK2 z+x~WTYMKO3kN`jbf}i$?e*TX?&yV1WE3axa8X$S-U|uT;q|EY#sgB2-s;Z=vN&+Du zO2Y!|2EoTZ_OYAa^S)#x46vE0x$oTl?K~M7V?q}8`h)-UrZ?`Mh2pGe z$;ru?nVFZoK&0Tu&CEe9kN^!Tlx@Y%nuiy8h&~Y3oYv!>uPagl2YbHY0%f;8e zb=R)B+5TuKB=>zk&C|eVo)Z8FQ0k9A`H7p}{;rvsg_EoOD4IBSaxqB~Aw&>2!04a- z*`Ey?Q3XM>Gjac+{eka&FhqU|+rK9zuXn=ZecNJ$yX7nkg}X66{7#)@(+1+;Qj2U;c6fMhW?(r(RvwS`IVXiBkdq5Vd#Rzm$M$cu|!t zAONOn-|@Xw&@LF@$}l$e=U79ZQpyj!BuSqB^rzQVWsJ%5q9`DW(Ek1V5n***%evy6 z+rgd?5{98x+vMY6Fh}k`Kp8f2?dq$a*pPcH9`d_rSKL7wA07*naRK07jy|yR{ zYv>-1xJIzL`am4V2M!#Vnws%_e`#rHetwo$={Ec^XNJ9PNiewk?z^?td6MqlvuAnn zWEh5wk#2YOagTfKo;|ydE*>Wa{^oE0SKzjaEcy1gzrET&W{|$)uCI=U`RBjze`cpU zRhD2hon=^5UATrfJxUEo3=JaEF?32uNC=KdcXvrk3J8cpDJU(1bSd4<(B0i2-CbvY z=UnIS{9$IX_lozupL>O!;l6+s!c@F8>bP7D$jz4C8k@n)aH6R(=*53H{=*Mrvz2jj z-qt9&CPf&BPPzJDV`Z_SAwB~@gnmbVH;yVbn*HyL2ruk(j{IPSXsHeWtr~cB8S-6& zrV|dRcs&0-V8NO8usW#xHKXsnAq5cr)NQF5ZScf?qPG(9jr_yM2$7Q2YH1d>6E_|h z+LDLQ(_;+8WaBEf<10Kb0(g)@T4%5A!M=ovF-GiXs?Kx)`gax-uGCrY3MkN;8!i@n zI!LwXrQ+a2=+KP4YDw7CKfWP=>r<&S6~n|EO(}6tampvo63?=*0oJTdZ_*O`VJx#g zuqnxdayL+Z6xZr^F&_rEM9Lx7ylKt)Jv3)Ro)G$W2>`oJo-u!#{LorQYMF>j< z;w3u`It&eXWrhhk)<;Jl49owDAPF&n7*qClVPgcWvl}slyip2!xnLc$zJ9YSvty&? zlT;xnL>cLz;5SQmNPy7SREMV~_fQ%pu#)96LU?n}io$*E&kSdb{0$pEJ z{b{a;B;dN^%e-><9aI}iPwxn0F$AoYVgwY#XfZI2mHVt&dRhUmv^J50Gi;cdnM&2H zgxkNEm)2#|yKcUK@P||DlF2ftn1)7bFad${vK$frQr7zV)qI^o=5XJsxIFTQM$_a8 zb*0xW=0x_WeNn0|Z$^Y)30KduGX#)Bz*t-48v!rGyLcwEXZ=b*^>TcZ?@`o$iMmd;*+>RQc4t|%f|ov0>M#f z3Q^P|Z@yc=LcytCH56vX9x-v#wAW^uIUD;8Z^CNhVnX1z-TZu=C9LS1{aD(R9%;z- zW7!EhrU)$E#QUDo*tuU`3E#(D{(j$UW`OL?REQn7nZ|AG^fvcu7QD>Uiy}5XU6bw9 ze4j%=Mhtw+wNpwdU5Q%7{RP87#Y`M-8$Cp;dCh5{ed6VQABE?F7DATC9vH0>ssAYz z@glw5>v0kcTgMXi4J8Z53M^eIR@koXDU}@)0IIoqPsF2EOZ?A3vX!SD{2hM>;8|lU zD^D5B85>Spj){fLl7-CQV*IP`YZYeMDP<|k=$d&_RAb;uchwb^T*MOfau7EpX68_{ z^Wv!%tNZEY-_KhfS$smz6%%vhYJ3)p@2#^oGR@0HsS9iH#xr=00iMhkK(_)b@II*d z?C$F%CEAa}l+F7!rW-DtJ^?!P7Ae1=SWPJ!77LxsTV%}3tMMXHuj7GL{M~?v$C}7(z{qH`{?vD7$33jA{X|?U zgGHfj8A>EBA?9;DW?J?{Gh;17(|F>>uJ5IZ6f-j=?Muwr(hCrODjcG0wZsLKxgc#5 zAp>^%@DbbcX4vkH;LH5Q0*8>GUhAQ7xisQIjKHCs3Iw*>oUI5N#do-=Yc5U$|+-;A#B2D&Q-W?n9FI?3uPgU5tM8ADYAZ8$u zFQ23e6zmt?_nZv7lm3U3wpv)o@i~Jy1)TksSrYM~>lyPK5Q9HKD-%OlHXPC(f3j$uCmH65C2)GfvkO9B`1 z&eX)j?r+3^EWYNdfuW9LJ3hm^oF~eUkPIr#fk!LfbM5+fJV@SQ0o8%-15bW4jVA=E zxv+eG`Z20C9C-aL1o{M1)ew376pQC#F`#MlO0ah1xKp0y>_Y>Xe$jo^D4)43tXef& z(Kex45SscPj~{(obg%pKnSaUR0iXOP*#=KXpP9P>Dl&K4exhRRReS#sp@fvI?Rb(k zjh^_Lf?G^>|K~>=aHU^`#6$64Q?xAp)mb0_DH1GT>bb<5L99S&Q62!Xq}eKJlWMDt zo*hWwka>8D)GcAM9=E>Jfdm!4iZkj+c)s9eX6_Zt%-hc#4aO%vr*gR)k zwXDUO$G%cecDuY7eWEMGwtPnStDEp;X0&g@Q;B~?o-5CPu?)IyU&^fNvdPjr3KnGi zJFES%JL_Y+<;9GLOYqeLZ=A9gk3!v>3s-8=!a&sDm}M+NEJ2EkO8TYA9L=d%Dj9w1 zvu>lirU3W%M_!r3ED?R!skRL9Z)aXH;iIaFE82>{ z|FMSaE>xE0*K5Xz6JGMS{K?dwdze)7>Wb@sa_CjS^)FejzgzH6vL-ztH#D47ng z#ProSHNT#t)Q*l0@PuMmqD3NCH%v_sW+P}Tm^nT^u4(sluzKg(WNqDM#2Wi?^ER&D zc7shaVRbAmxM^&u1|J(#wq>?Lut~|Jr=Jm|g@v`OoTB3~18dgTtYDg7;493OJ3Vb+ z(c;P3hab9AT<{qiHJ6dg^C**rrS$f$s-7IMv&k4LIc{~=F_@Hsx+K1-U1@a@4{}a@ zmde75iqqJu5W9PZ9~5XRvdtw*(tohvZd+kw!yV4ph7)YF@d%B|#jMXKJ&_ z?%@5J8d3k`q^hZL>iIj*Eat324J>15t4$Iw>J-N6%UBqoPyZ658M}|f{+Vgi4cSN_ zx+GFAR(@HV*bFXtXrJTS z(_`V-HlNV&o@0OfwU=lhI0%EHJ2bO2Pq9RX6TZUGuFG;SiooZFW7A{vqZ1e#r-;^H zjmq1H{f(i}Ga;!&vIvT}n>F&uqZymwp+Zi2+$8{qcMYYb#GQz*cobR1y}#p9Tz~Lp zkz|1>Kx?dt5B#cR7sN;&59a@rk(89V5(J zibe|1{TM4XAU82&RbhwQpBDk>>? z8p83u5dQe=M|2QY4#k%YKgHbd5yWBeNtb_l>)#p=F!9xJ@gt(-TyEcOoNh+W4gKXn z!ybz7Lugw9i-cyN=tZf4=IUhR zADVZ+Tpr}pNd+zOC@^K!gQYOwU5&a$N6&AX?M-4{|E1ESvG2*`6kvW$-RUh_%F45+ znpteeAdK!Xj|7(9PF~MN!pSRD*q4%&k=@n8?TI>qgg@~p3F*ukQ>g{NCSWe~xl~;> zl4ZYAxBor-7=ns31sFB@v*N~Z1%mJq?|^jZTZmqoS>{nF3py-$(N54bIp%MuCbFs- zM^g4478y4pc+~$vc2Bk(fxBju(5GtsJZ+Oy&IBJk3QJe1kXXeWd~)ZE{J6e8ez$nF zuXelUia(+#iVQ+|Xw4xuciocs- z&?UK6NI!$C4p$2w2ye^@P{oM&Nh<9Us@NvV zys<&nyu=trz6jQT=KXIbwI-t+3ZuqQvbG4q!`DLdIq8bwnw3T5p?JzTb-t=bU5PbBYKi;z2LXH_j zPa=6>g+zhyKs>?6kp-;)%p8jSk{=zwk`IQbb0p{V~R^S z7EK(RY5ZK}3$#jAQbW=cQL4JLcMOUPKLs90CNn34sq7yNOYx z9YvZH!N6f_Yw}%|q%gKZ%RvBh!!hK+{W&63} zbzF|&%SV@u-z+{I)B#>!RGrAH{`Iia`LyAelzJfk2aQE?IUdBMnjwaLuVNOZG?o(D)_K^xIQ>as@ zxyXEbZ1UC56+#3!TzIgInYrUhrZkLC1A*2^X|qVQa%pW72f*$(zOhu2HBw5?fB=S!Rx}oiM8FG=&~F1-MVjJ+kwWK0{i5j+_&5a?RT8F0N~2hiTWykSw#?ljJ)po?}MFPoN8?W7H@#U$NzdXlM1$+bLrR9t>9JFTU z;m-yOu4?isDjYS&_aZ|!W3bfs?qARJ@Fn3@Oj zaYj2GeVmf&4ORN~l(Fd_>=+mZFbw&KurAndT&F1*6}I!hvaV2I!q4KTIM>qDEQeOk zk#E+IBOM+a6^xrEOsl+MWy&DjnD@Ul*Ls}1Kw|c%K|@Ihx=4$F{)}QMvI;d=#EsGB9VsRrDzH1X)AD5XpvfiofmFn!qo*bPzT*a1U=q&;Lzu-O&Cy=E#8Dxj!>_my1{5c{+5sP$x6m2`I`y7-aw9=Cel;{oS<^%XW+^By4hFkAO)f zb4D-*RxY)f-G==&fZFgtCfyyNrr1)!f842KnzgI7P4Fp2AT0FQ>CmzEsddf~0GlBa zfbvpVuTE(fkqbsJ(Ze3xXfyRRNUejLliG<|VLN=;F#+(IqpT?c^+|!Ov7R-NjB0qz zy7v~(qDd6A6&9D}T`-MMqAKrB=&xAWT5qqJd>#T4bzY9)u}>~l5fca~iQMuBE8TLu zN@}%lF6hEp<;6_gY=+~-VHL$_JX2eP=*703z=XD0rokghX}K0~pu(|%As8mMdGohG zeCPa5l-rJlJY{9N-BbR3J2Db|yP`-z`I_Q_xD^YR<26ObEY1hejhMkwTJr$VhX|+6 z1eh>){OxjxShTE~eJ4UoK!NF1SQLqyHa(5&)LqSt4Z+U%`1DOU`cq7^$D}Aqv$q(U z&(hOlRQ?WQD@sk#KL)hb&G{#^88IPa*y!b{G~J;)0c9IM=bdun2;+D2Tl_1r%z;&kuE z2KUYJo{@CI<2<75yOyq#mnvQ4PIV5B_oZLB8h6(xjY{HN5~_SIviQn;crv)mbP)Rf zSEN1{hT>vAtTN%y&_Hcp5(K*vjg&Dm_U()+Dm*h@M!SydCFL`6+Ds+qG}kcir~@>Z zfeXho{Cbnb0$r0wZ>ZRAFKAHPrB5NglW08d{hxA+ z!tuji%7k|~N<*f#>+|zrW@Mvs5R=U1!ui`43~o9_8Elqx7e@sEMqo#Ox zPcsD_j8P-jn?Oz;9yDtH@6!vxSG`iNdcxm*c%_XdYng~W8eY$8sPv0Cn7HI)RxGBE z>{UK*{{|*EJE*7gR!pMAl;%OjGi|u3q)318$=bXb-dLr_OQ7#9PzaUW>{nB^oqY{a zg~)XM-s(C@p^892G!2l>y7y2S#4-Px*}Opp(r_`ui4%>EcRWuT_~7edehj1u15gSlLBPxVc~ zbW}xB(FV3}QJrr;-%kk%NxF~|P?fk!NGm?Ib6z*^sG+gH7Kacm>8T$79VeQ0AmaG- znM{FYPJb)U!8z#Xny8f+rXW!G5B)IcjH2x?edZ{up#d^Dh8ugIA1$86L3QoE;9~00 zcs`i6p0_Dj81g5-Zm|nGoH_1s=f3`=YB#vs+W2YyB-(G+j6C{h{l>Okc203pTH`4k zJgn6@9qbrg>;&9f{lu$(Ls2lEMq(sm1<_Sw0BCE zAH@uD-aBdt+LVtaW1?X4&8{rgH0e;>h%C4P9BZv&!mT%AOE25|++F>FU<%Mx=df3@>@vi8gsL&(2i26n2X;HG1_o}hivD3q~V9b*++OE;K+k-*B z4Jyyu`RkQsx@pfN3C{ybmzDptR|k@(&2xRX?PJgEFL@5hY;MuAj$4m5>#w|N-J)0= z|44`LirqA&TCxTfd%0f@fwlAiV;dVH!2W(A`fe)Pz-N!|g!=yCp?0s>$zM-ttENTk z{>m=kMCc(`K>TKU&>t*jCqDZe_ukQw>$TPkP@jed&=-}za9L^3Qx9vp>URR$=vsCX z)C6jrKkX}$q@e(kb7n&&KObF+ZvzjTanO^i)@$wKGa!E4S{G7B>RplaQbZ@kf}axH_z9BIew>?u}E|!cu6><-rL2DF%9%1(+64 z4kP^G-IAc^n)+<|b`H4|A18p~=VoiJ7J<9m^%`C;6Fw|oUtbp(-RL!4=?M{pF`7$} zkiLqkc-uoCB9=NZ+>z-e_Hh2-P_{}y_U^Y&gCi6(O}RJ14714NTdfyAr8R2?OZe)> zaHfLfN#fw`R9TwezoA&I#$zqe@WM@zSzNatpGFA!n-V}xOr{|Hyz6vN4=azGr3c$# z;sh#CzDt+@H*>;x$d!7=9wpaeuahv^KuxhTU1rI{AQFfZ`oS>{FIsi5{9+#j6@GYo z_i`S1cjI$6T?HS|J>LSqcv5u0nwh$~x~HG#LtgxP_55Yx&#OeE%elGFCWRC9J+{nNEOE_j zzc_X4R;T^6P%Ic{Db;5e5EGN2KK~R4X1ZPGUCDDFLajPpi&oi!X3u)Lq@<)o#p}D_ zYta*NRLZw6>?f{1BlgyA?n_IO)Dr${3mAmE(|#^6L8-j)@ho~wu6&auSa2)7qip}a zt>@WTz9x@Q)th%&JC|0{FelZ4>qX(@!SvKn=xdhze71-=@sgQ6W1fGX5K@ri)^tfq zTTEA6T&xoReL2a8LMtOX=#x#p`rtSt@u5DW|q_=Aw5|GApD6F()*!~T8mg<+q}!*{~FmZMBU@rM~Z zquW;Uiw_cmC*p$`SrlgMIb@sJRuTNJ5@Arc-NRT9aK)>-xod3r`M32kpl7O1C(#<+ zYU$i0blNZkBnyTh&2|uk?$~$S| zV->6}FCO>gVJz0wah%LLJ-kO`3k=@e?W_O1UVBFIG$iD!TEq2Lj;0p%Wber>mgGY# zSWEOTnY9%lvNj{(ZG~Z3^NAmZb%&h18m$ym5jSL#Gl6MlY`nD&Qep}ZBPwQ3dna=- zQp*SbVMQS)kU0XaK8u!gX)9^4{9t>pCVAB9bjRHkna8@LZ(8)3%j@H#H#|l3`CsNd z^?ioRWD>hGU+hch&nVJY#p->Kw=bW*4TOXJ0JJxF#XHa1_>s5GXLcP#GP=paQKS6P z8S?$>a^wcSmxnHkIXCBft0}l>AfuFS5KxFZS~ug8|3=iuD%7{tS2MZJTN zhWeLGItVgD;OR_gZ#9_cX)vWxPa=rkHuAo2SJU@hkK=8I-L5)3lzUDVQ+5kAY(+ZV zay$8tyO%_=-cC5(ZQt&;;|5$a|2#qFSTNqz)A=0lD{qR@`V5LK+?;VQYMc_3jQ>9i z@cr<6vg4`5n&@+6iixQ0Ye6}*E_YG}InHPFDzMEt7iwf|;bY|AiTJnJNX$2lo6i(i zYKk>wSu+KURqzGhD*h%yO{L__@7`|7c&dF8a^uoD$DF&hR4uXsbv@MspnJcvzN&OW&zViVQBu8^JWv7Z6fSk}es0a{g5P|{u z5n(36LY}c$P5ot!tHR7bR73vaVnR*ZTEMmg?k~r?L%N2CJCN(bH363954>;K4UgV; zGAZTNvY`(Op@ccQPE03@dmmv>S|~)*Vg+(2gC@go=JXnSwX#&7i0aNJ)Edql4*nGv z(ic|b(S)sgZ&&jVsv~Wv1>$*SUKP;~iZEgGJ5?p$8J0}jB4obuI|x#;7Zem(@^ME9 zN3adhpa2HMWZHpiUJy@TDTm_Mtt}hiPYbAIf6Nd>udSm40=aGm7el{Zx=??qEk&Ri zf%yQVlcdT2=KGP_myvMKMZXJ>F#ZJB=HW&j5ynrh;-tG)$STLZwcAfhOk4&{LROx8 zy5cV45v%vs)t(0n>N%=d(qIwFnW|D03cS;>uvEpJbgF)%n@ZrtfdlwgY)G9sX zt2tCihT&ZSxD0~_gyD;Y+kM@Cx*Jyi#um5 zSy8LHoi<<1A+a?zpW@?5Q3;z^Ff*Bi36RS`>4&_6$|4`(PCx*`jZkR0O|M8~{9#F) z2eji^_lhvVOC!j>a5+n9g*vld5q9=(im)9!cEpm4;WZbZlC@%fslK*03=g}@!XizH zb<;BYt6Of?OoiR@)NrN?HFI|ibVYb93m$6;UDG9jSky!K_a7|N&)c=#?z8>)0r(_BS z;devNhGA>#PhguYcp}L-Au+M%#P>k1?x5*m3Eauz38NqkSOA$R$9J$7F3>ur*-R`W zizxTCtr*`nR=K{rUt62sMAAB58fCC2d%x`MNqTH)() z>~!5Be;L)Vz!yr>E77n$e)ig8yGiu_`dS$vuEzh`foLkRW;%m~2c@AQ05(W6O?nf+ zF5T3wcAa_dPVKxsTJ!SDFfjFHpDI33!-=ToT;#89PKS`Ed8}*-S~+IwJjH)yhRk4T zhyBK!%v`KqwF6&t%#T(k!}d$-dJD(jJ378aVL17&MymOaM88;PRjb{~uCm5xSXinG zxTyk-OR8z&Hk+b$yPwTEU7BH+Cz21YdZ$&RTR>ouEvTs~7;hz$Lu2@o%@8uA`vXge z^311=2YDi{_B8|!cG6WLwv{ijvW9AcMvRLQSqM@An&7K;x0JP=vGF#y*V2h(4O=x) z(6jb-a~)@7w{-&V#u0E;!4_NAP1~=28Sa9lYD2?!N-tpo zaTD0h^hMH&MZYy=y7M=B{soLD&zUe1=ouVnW_VyU6<)C@lO_m9fjA^_X|XV)TPYuc zl!?25mwScBG~~KCC*+-s;3_s_d47H=2i2bDoNaNjC(WX(uTxx>Vw!^wLOEJgjSEv! zpwyZ;r;5f(PK;1|16~(JCz+UiOj_b4hxjMq^StS}VmAKmp62P*;KN-J-IaO4)_;Te z-$wOByuWnW&gf?QVButM(Et44euevfd&Wv~=M%T@FJfLr-a&i_L#nEsg+)>Ea->u3 z5lZOa)>ijmz`fI|{E(wiRv}uVV-;vJmy=6VSBfu&qk!CC@rxQ~k&EHMLPeF-xN{7IVP;RT zL^f87EPLFAT7Dkf5YR)wkHBLBCiXGMi8vidf{M#{s2`@{b5tvyHfGup#v`DGPUzQ|{Sa?d94b) z6T?=8qv+b@+**o^$rETxhqvqz;;BK_{V*_=+IO>mqYT8nrNWrs7d0y!Mt)c2y4yck z7~2xQd(pK1U9X3?s(_r~YRB+m=D#7N#&K-C+W2{$_tUif^XioQ?XdMJ(n@@_?C0GM zy_5|bS~{qS7fl6)ao-_{JdcCur|w`#xL29&i!L+h@~=(Zyupqu+n!O(N@8boS#E*w!XLhN9V zI=`KwDpTY^{mB3ht?-uj;csRMBH+J8!NpKhL+;N>Se5^)NlGy{-%MR}8M9|kTOPT# zSOe$~MXuZ(3}_Jp7{7~hIKu^NzEPxRg&e)FWAr;+0RBMqO#3J-?#GkvAE85(JQFhX zAw!|8%HskqDUrjEg1-m;s{7DG`91EfOn8g&AGi03uEhe{|G1DxnP_v)Ef*HudPVX& z?~xzjLcPRDR)%m>50yu(0u147iwQBUuK zN)aE-U?Iy8#D-m&;;)tGsrn;B5p;1mH&9+1uOeGB-BEv#qIN@ZB30 zI%{x2#K;iR;qO#6+hC}s1V-V5>HjPYhbc-1ZURn08RD3{n*om^cmU^tPtcGU`OFqr znFUE@ng>}|7vUotOc@qj(Ouy&SY{kVe9}Epv&S?58WaZwEN8K{C#IiZG!MYu>w$S* zs~L$f4^#%mA4*aGRjS2m&%wrsA274Ui>$(|II6=sE1V$P5Z0s7!hdd};40j2IZ8=t zPn7H6ln^*`{-O$<%Rb(tSr+mUo-cQnE!4nZ%b)P_(-P#@dc}Bjz_I$CX3iwlRpSAV znjL4z<%{U6!!E8?y`qVknVI%>Ou)M6+cWQ&2>7NlVFS05<(#D3$UpHJ9AMC0Av3m! z54>IljtXU;W$NA%n+D3Rei6$7^a-q)j~GDIu`d#lwwWsIvWsyLV8j`sY3fSj(U_%r zHgH_IcZ3riKEJ`O=C`fxC zaI)!4nyit_9eq~wO%dgik+u5>)kaQ~^-QKHThVojQ7@+1zgwQ3D%=d8AQJ__()DW1 z>|!>Va84Qvq==Q~7(iccE2xJ*2foDyH2^^L zko{qBMUXKbxaM4JDk~-O*aA`pr2bGKa9CH-fjn0ae!wA-@BDTaniYkO5lGGKMnfls zBVAuwiYhMR>IV#rNP){N86QUw5jo*su?0%&$Pc1#1oT2W8jv`+Y=S14fGK7kX*Fp z5I{<2%zvr*6CM#lhFA%#GZkc<`Yv8D8fu0>duS7O#E{Yj?4?4nvcStxo|vnC=>i2z93WKb!Hz%UMhphy_(@!l#e<5aZUk8BNO4K56c+;GlL z73NpfJzlWnYj5FV!TV}Zc@c5GrUDgO@jCeE^ZhpPT!DSi_gpRNtHvT*PanpEM@fN3k0$l*Og-Rq2B9?@m1W0Miv10PBrw9W{wz{jiby_OnEO79N(lp9 zilOTmT`HL4q$RILb;)GIdrj|x+ZCgE@~8V1hkUz(p|2Mv6F{;vSZ$6{Ft=R(dce`^ zjtog=_iD{+vHIekD3BjOH@3K2+?x%4ao#c zHtYbi(CEidfSMNr%bMVEn#Z~to|;3KdA|Q`1f79uC?gM6LjKyDwqF7F&zX>-Ay;cw z0argFbnR&Mc(a8!%{j!TlhG~_?a0))*Wa%bt7 zSguceJ_N64#dic$@BQ>&6*8RPIeY#*yU}^}2a$`_dEaSmr`JwaF zToqi?^0q$;itPJP=SMt?7ZrI#nkLZ5P*DCn@><_%ia)BZ*k^kzuXL^P;7Kb|+Q;KS zqOn?*_4`{krlfvLopr^p|3yrefT5jY5Sp~>=}PtV-O(BRFySn$t0U5(apx*|L8m$C zL-4W>uJPSW1^O$8j2!LbCVB@ZhB+1#owC_NC0<0PmP{98z%~kK2F#t8J_h&8<21i% zZrpq|@ESj3WY4l-(!eSWM=4P^b9eEub7Oc_YQz^Ba2()! zDr9u?6WZ*vcy|8-tjE5^zu!4%T)%L-g2E3sz`ldm#ac|xgWD{5i66?94MQKp8IX*^ z%ipOm?0Gm=esG@eHPm!Y$83IOKtRLuI6arhf)*}?hiQn}{Cmw9`6c{Ip2cI9Ato3g z%M*zHCM4Y#v;1>zhJ}GrZgM}2L>5jDTs|cF95~k$Z`0wj zgY?I7KYAnprDs6HKtsobh$jr`1;G>E>^?IV{S|5&7DATPBJ5adUsIqxqER=(IjXv7 z;TMqN!$dKwczSm!VIq-PbAq%SNGTgM^eJkXsGFFj3v$_NfhM_NTC1DWhrmz2J{ntc zX6%jB(ql``hFLmDvPD)J11BMKRa=$J;zyny2MZE~yDf8#$L=M2_}2i^pZ;h#;AR&l zkvlp_ceo;yF5ZMN;X+bqVWLl4PZlTgtmLFMYOUs`u{taELt2&72gL>&hp<$Z!QiNy|z&mc9V{?wX5fxo}OYk{7I>j zm%%wXHc8|*I_oISfy5aZVv(KG&xB%rdo%8Klf`LCx_kl^wZpq9{wSTD2?-D;P|a}# ztAU>onWI}E;|#iZqa!0-23l%{uaA4VA5cP#!?xF{ueb&-(ymT(z>LAhRjVRZwq`l1 z^X?JM>BHXjD6LB?yP*f-Wn^?1m8bnI7+U`LEn3ohi0bp7_b`3u0q>^U>DSR>o846l zZWk!`orqFT)FUfR47-zUIc_hNowcyf+dB;hgi-O)`BC_ zZ-ld6+tv%{x$>V?PfGlclml+(HdDPfGAUiumDX=wqozo;^pC&}tza*IAfklav(D)H zhf{nono}k1{U3a)=N+Hzm@D`3X|JluwJi5pH%lgbsx(ikol1OG$_&xd?pqFirtT-$ zSX@qZzW6BBLTUxN4k82y5Bgs|rU%?uHC#9SZqWR<+a?ROiDE+s5CqpH{vFD`&rOT> zXNCCjJa~MrYr8dt$L$NZBFW0^?@!N|+x06tI|->pF_->Ezi=AGzj$$sRA4&6?fUXW z&1!w|Fm<17{m;Ezb-TSkA9}V0Mk@$?HW*cZCL{oF zv}7J0n42u}A*W_1ED8l$C12kf2Hc&2%A8x&lL_v=z0En0X*%#d*^Z>5o%oq7 z^2c-I)^#97V(1&M@aFL5-mIr_@5Y1g!_A_R=6Jq_y57ND?RwO#+KctLFI*p{B|MEJ z19Bh0!7t|MtsTg8WN;TINPE`Z+Nz?#pvVC{EuTdTtUGAzIJup#y8wk+7``qpf2ZxS zrQcM2H%G?8-{!qe7Bui${xrbwx-@>(pdweUUNr99wUu691U&4!{yf|DO&}l- z9rWLDEo*5Nki6;ETktya5Mt|oHiIpEFTdu7yg9VvD`PhK8( z(9OpB!lhe*TNreTQc#QkaaxaHRoB_Sq`SSRBTL#ir>p%p$kp?8?g{TsfB$Y>*)&mC z5xLMi*dR^H&E4Wd@PF4cfNC3e9SsYSB*SULIu8d8jYw^H`t%-pJ$2NrJXMt6jo!m? zjhesx(b6v}$Lfo?QPrbo*;=yr%lbi}63puEQI4>AXOTh0O;nxiNBKH@E3~8jl7Q8d zpLb)Y&p2A1v-(PQf72+?gjdtd?&}TH5c;yv8C|w+8iFD%DmIVZDeogH!yo58$}bL= zC+n~A+uW9i8YRBXlhWNBc7ht(=5n`EwR|L;s&vt0fsCB;`3X7;yt2mXuf zM&~04kHcmOpTQTayP9Jh`YyBmRiqa2r&dijdj;4&mN&S;|K+f&7X4?FGef2sB2p+xI`b+h1o z<1ibl^Y`#ImC=3l!%6$*^^1{RW6ijkLrd4&xf5DGmD27!je=~ggV`o#?h*e0f8Y9K zi|ItG6!(eD0X=<~+l-ks^wPgSsRhQ&V^vhC8D=82c1|}%CIsyOcwi(Kb7j*BY*DL6 zM@JmJQpMBOl9;~<07&>MB#@SNn}3?{;cNy}96f#Ix$uq7&v}D(QcX=SU_mF^tKQah zUh#=f<-zCffKI9h@EhLEEI5QU9>*Wtp6wWsDGUhRDror;e(xfS%zoSFw|ZjK^bd>M zOs4#K9Rh|zj78{KNgQ+Vz0!U1jVfBq@Q214PUc5!0Q(XhbDnPAV|GuzPJ+*8@Wa@2 z7*b1)xaLec>wlhklXu6)oD5PCg+Q-gRdC+DpU+ zb&suq?vL+*uJkVBllShOetgWSqTN72iw<5?->Ft%3gwrv&Em_M$wn`cCoHdzkD`t4 z6BfN|V^YI@`fgWf1+Y%EIr%w_o)~(#fXVDk^sLL{&0_yCby}xS{_Tp<)pt3Q7B<64 zDYr9|O}qlMn)aTZzkEe>nJX zJJ^qS?-o2tS@s5vftz%jx>0Q1Rv$|H1fdQ1zRpBRxBboYvhDg>RH!M zFwh-@5m=0zM2>cN4|u1=>y)T@%c;E65D=;wvX>t)E*w1+SNkzQVzcWryDyfW>e-XW z-1__P+e#qBxKW`!g2O7?I!eg>t3{hz+;1IS2m>=ptE>8#g15mkX=vl7GT=cmlTO*M zkm4bqw88w-wW@h<)YP{gKOIJZc26E~{}r53FPs+c4M~wzopn1} zVPg9;n9N^QXdezoC!OrY&uVgtjn== z*1e74JOR04HKsS`oXJ%SuA^rU-o_;w=>F;AYFr$&`UwZ|9mc}D-i^#5`AVzN6^M8b zaERv(ouu07;V)hof+>5A)uJg|%>E6TfEbI!Q6QM^lk=(P5B>N1pa)29qi6b?s%YL= zHp|(qX*617uFITDr_*$4$8`SUVsaE&C*>+NFI{902gV%F$2S0m?(6+L4n+~;NEZU; zw9`MyPV@SVWiwB^+#g9{@hU4bJV@^JF59=p%AlB3x||Z(c1h0`OZ17tlr7q z1*Czf%=|Ai54g`adf7_qo@r=gx!V*y*X9wT#4qQn;HsM+C^qoy>zi{xf~9}Zmk&XF zXEL+rr({wb*+PzMw1S2VftlAIRs>4C`%ls?>doV(>))i!x$XGXSQ}pYY(8w0Ybiv2 z_Hmr2h%6b1OBY@*&EGgo<-T1TCQn(7b_tNXiFER9`_yz3aDS)+UTwrhO)A$kj;^1@ z`IX4dQ%n3hCG2h~e=twpDnVGG5BC<4wEt5Af|ghRjQy4e_AIz1mk@1ONzxzhdF^{k&-$6qHrDU$LTr+^bZL+}y0+VHH$5$~hMX+xbI zN4;KDSbx~La3W4UGW-<(?R{^gq}lS?U~x^BQ<=(gL_qbefm6V&QT;~Ngo$H|sw^$Q z6Uh_Tl5i0Dv)Xc1&A&5W0W5j^-yH#`JJnyIN;xxGOyjq>@vtzuB(7N1?xqDKH{q6* z`?c%EpyUB}3FMgu13S(pofakd)G2!HDd_+!-KnK9bO}4$RH>S`q9{f9T~7IKR!2u$cY+I!AoBeb%Yb zB%hojCK~-62SqbazM#NDKV)UHrFPz%Da$=Da7K$6NF+;pLlr zbj)&k=hg`P7PZgQlNgLps@>;rB zyWD;w5*1UY$_O4A5>r#f$&KYC0bN_)$jI3`i#aJsNw}JnSj;A?~Hq- zIk62TM#cOtW2^D<9QxLf}sR#@z;Xsyl+Db>D3#C8}Z}@omIME5_NFG`a6)-~3%RHx^<>gj&uz zPc8D>@7XT&MY^b_9%m|{=X|6y`X^QJa^_fN+0h@}FhN^_OJTSO_$E%v2gmiM7ErG} ze>Lan^qj!E&6k_5iy#$lQUJX70q~|ghy_1X67}5VhJ4McF_!4(f>;>^3dVD2rK0Wd z&)ejC_hAQL%}$P%a3#LY1L+1G9vF1L^q*`#HVeFghSMvv>1w(aY5vAz0wKD)Fm>{E z+{SNQ0dC#x7?#1oH1{od8A;~7Ow9O_cq92`0~xF=_UGPW=irM?`yHHPB#y^xmEcDT z5x9fnM|yW!w-0NscErSAPfXFbz{?qgvS(!VYgpNXuqD~y^W=9 z3_&9Px`zAvii`i=1=@=Un(GY&QQp-6NH3A#r|t;JC$#2nC#Hwj_sBPk#mlw`#B{OZ z{nvqFV|?z#IMgxEVX+Our^+=s?KdT>#@Fe!t=`toNQMt>Q?>dH^>b+9YVR5bSlcR? z5p4!oqlFGsEh5$6zSftwO3E19~8cr_NPyu^m zrn;|MAUr3;Ij@f%>*4fgi9zJx_6dsw7;&%6ZcnMmD>1DLu|y@^2|{YwLY(aF1xs37zm>qw*77R6#j?oBde=E8S@2V>9%^rC=KU|>FWw0ATv z$VhV_s!7lY+$0G>tEqx49I7ga;iEYYJf-d+9Q#)iRvIf3)>aD~JO%k)IOO<#I;qaQ z1;}_j>v{e+3s4v$amp4};+8Hlk{dGCWV#DKc*|7R3wh}(`ic&bYZnsW=t^*ZM0{BT zc9C%?*Q}26g(1l^tH8TVe*WcFKJA>(T;gk8&D1E))Cde7B*$d%Juf2>BF8WN+gW>z zJ9*tsdFB1?q=lrZO*%^CAKT5*1_*U{)Cb>xk_Sb#j12G&hd`FXh~&uQ&TBCXn-*|Z z&Th?}r%!H1>-W?>J9d3xsaAr&6LwkwugaX`LY>Lrw4>Cd;Fu8>h<3zv=S?JN*%mhW z@}$y@A`90Cc-!LMzax1{N(c_+fwmaH$0BQ5M_X8yE0=f#=9Hnc99-!dWyF`~X_}%r%tBv-MWF7j6xg0n}VG={i6K1{>6yk_y^@CsDRC z5O@Z1A}Bw?vocTi<;Y@fHEC#s&F3J|Lf`-_%7QD{Qz$(J0c&zT96Q84XHm8+@LeLD z91a+YK;TZ9xC<4+VQ|9$yX--HCRX>ruG)pW88na1*|uvJzHNP@GlG!?*s@SAa5+;! z*6BlwGGP;YL@Lm0oGWhF0tfYuN=1od^zm3g7k1-p{mw|=$zHp=$5{(a$SN*zzG)y6UUYCh#+w*0!l9lqC4dzv3zB*YN%vL+YEQ#uBtACNsDowk^;#r(SSU9I`#t? z?k>b}DI3s~fudJ;BgjH|u+DQ(e-zWn$j_;!k_u~D6geh_ z(rVs@8989)&~J}2f7y>*!;WBWim2u=s0RBY73kz+Dm_=+ zJiCp+7I=ahnZ*^|Pse{6B<2d;IroM^KO4fVOa}x@=|m8Jh3QvdqxR%?h(z z{a~o}R4eh#^I>LXG5R26aqu@FGwod)r@f`LywuDg7XOsHky?_F1DgT0#~~;h*OZ*6 z*Q_*O2-|KuWm zFcj+Lg8P%mk~lG@h)DI2i~x>rjJ!HL#Ew%z#A@4Ni=39TOhZpQ7r|NXCvN*uRa-8fJ9cftW!D z1c)C{2{;#-7|K47?w2>Flt`%N@Gju2#q(T*90YHuPUBv*`4=HeyEB`3w2IHBWyFZ$ zuJDN<#5d8ti0HOxm@X*$y=@JqHO3%&BuY@7W`A?}0=a@h<=M;^9Pl<{6^WjHjlSS< zAVNs*>C8_JqiRjDXk4rwNxtl<+SiNM`_GRaAmZw#1v3JzcEb)Yw&RHKKEcD^b*2AS zY5i|cf%>VKNS2W$Fj>!{^cK<|$AV7ey-IMk?Lw@*$7G_=l!1{wv82yNOl(n8FTprs z35+z7H~T2mq+4J??}HmIMAc*DrLb6I^rg!Sr)w+hd4Ho*g>T6v12qu2WLh~lppOjB zx_Ag0Zmp`2_k+UaC56oXzzeeq#W&yQU>~nG>hZdGdH!+TxbV&*?~txduO-NDFDL8_~s zt5+btby66!cTv>4f*=_#$ty%bq(%^;Nd66kb}pf;{hf-&23MaR+!A6|7eaZ}wi|;% zLybJPZLiLBU{7v?xV>~W;bFQ58p^P!%Lxy24yOc7x@d+%K_5}0df#uOj+4ZzyNqQ} z;x(QQ&Q;2vnLz&R?@^G?b6n~^)xV+7arY#8_{NyyuC_HbehQyNLj!yNGbB3bgt}~( zWkF~+oNH^%FLbMjr5Dm$$vve@c(3+>DjMJWZG4}eE>``*BQ-p2G(}U!yn2gs73|Yi zn6<{)48sLOC!>6M0Mor`-I7zw0rtdWp2uXdoM@MyCoD~^!6@n@oKq9--D)S;)6Oz8 zwc+WuI>#7ujpJu(k#zq>mhi5UNk54$g@pDS#0>bPGTo5^#zlFi-uK)gdou_=aW)L~ zhF3H_3g&iTkPGI4DO|a2Ie3i3$S0Y+NiDUB86HQI?I(KzbmUjRL=V4wysI*na+n@V zF4A2Nh-r+@JI5|W@T4uKZ6)xnOK9(x4}d^^PEeqGphF=Ce+y#ouI+$8G|)SYOJg1n zH;#&fQcFXD?8s!?MR3gvAHSF;p+DC|DFMp!bkWpN&@$BL457`@XV0c?o0ZN$9zw;Jiz2TUDtRwu2%Fr3JsiW3ebKl7-T*I_UP7_Z0GEj(NDf1sh zVif#jy)%f$UO2lV4q|)|o5ePmC#ss72CEgN%0*Uj=L-EGs(=eCq&x-!)U<;LQ21I1 z;Pf5n)57av&=P=$Tny*d=@lthxN;goTVMa%Yqcvva`#LdW>xGJL=4&XFzeXxfLX0f zNkfZ8e7_cSKqKsJe|E`iF zJsWh<#M?FAI+X5f+Z%T+Ia&BJ0NROnrAGSj)e@A19U{)LZ*JSRQSEh=i^4e+O$;WE z(o)73DBeQ+$nXJp5KDDoFJzQQ?w|P$L3LzShgNht#z$%MF7-jFM8pq3V04n8<1 zDOETIekx@T+7R*Gw^%mMrN~p1)H9pyC(_mx_oc;%2us>6yd>|Hw44^absv}rp2er*wW-6*bI<}_acU-9etfwGfPSLDpERN`B+M)1*X&8&+uA<*G|6u zbkw1q+`Oj!1yIz@BY4A4ny4PhpX#akpVSq>oL|NG$+aY z6qdM2#1@GqNtC+;&()ly@<#QT-^A6_pLsTI1{7f(^5|6hxg~$PHtH6gybs@Eq0x15 z635AWR#jY8MV>5h(?(|?gs*JkYg0939A!}!j>*z@perAiiR1j()O~PZz%aznDK0Zk zU?f)Sroxji=c3|T%)=(hS2Wtr&D)I6hcz(;Oc`=U6QKk;j9`aqS8BobcT9i#ZtU&d z@9dbbyNwB!whqt!&|f|B5T4>`)<3-wE-q5vGsHoZYNpT8r&dm8o#LU6>CZ^9gjE~0 zY+R?|YfFzpjE=@hPS9!z`2%sGk!=wk<8s>b^gqRuV_B~=hS(~<0{S;Bu!&^jS%>E&h@jaLXFUMb7h&p~+vE^N;H8Dva z^UJe=3J%s`x>K%FU<_uT)sfK4B}0+@em7PpZ@bw0+24&TcC3;(c>B4dTe$K}DU=de zpt9}eaQ|ep2|}_n6c%h-qR<>9up5M*k0w>*obdy?iDZl9MCq`vvCTT|qq9&FWI z@wAh73bjh*kM~3$_t8hh2lXbB2z|>ed7OHeDk?Bg5Tk*#n$&XXbYt1NVFdD3S9;%+ z)3zmBjb%JantL2C*T9@v+tfvK%6y2JYx2t_D@#|#{9kK{vSDXv1#i5*ZzjTdG+uHA z!V5IbloTf<9K8>0+FPaG&GqBDbG}l?Kkx7(n9>4*41>W{mFK3e;C#6iHaeb`RnYo&Bw?Aye zWvMl(br^XO7a%u5hW=do10=Y^`d_xvY92t<-IXb*I%+BDNU>(a%%FMi9nVeaNsg7Y^NqOw<*ztYnL5=sOcRy z4r>vY*vT*WO2C^Ax^j*np*Vym@igN~OT+xt2gM>Lo&GNr>@nKEq#q)R(VCB zxd0=HG)Fk@*_kM4uI}yhjXn-aQ|yA#*PyZ26_*~S>)I7@sDA?OkGYkhVq$KvUdlM6hGfoR_p*W6NX}-SKxVg^}2KI z+Cdme#hh}B{Jj0A_XR=Ps(yRePBMIh5$%VMi%U=i56mi46+A9@z>2`NBb5_PK0gBM z`tV+@c`{YM69ju?}M!x8-#likrTZ>9_oDXgW$(^o6ex5IY5= z?)O&-@^gKZ1$LsVy(g0*_9Q;L4#t~wP_>i{yOqWz=cxv$VnzJ2OkYTP-+^Lt!qx!* zB7*y<|FmX^@g7ET!`mPdMv3FNV%Cs*YT3!pp7^{xOFK?9pgLbdDj^iTE}|bt07)lpaA@tFs?n~FV<_e zo-2ixrp%+Wp{g=wA0|V=;tR{Vrc4o_ia=`kWBycRWl$N*PkZ*2$Eu>#T;)dTtK`nf(H4y%J@poM%3 z5d_n>(=U4Dk7|`i9=^O%M~w5Wn+4col6pVeOBDj2q;jeK|6q$YLMlhHMzz*7g)px30et0Vm`s;h8qC7AbX12;mKcvd zD2R(RdL3@5{*g(9o}hJWEoH{GI{a@jsY#o>Jjs0!H!(ANV>4~h=de_{WTxWqS!xH5=>q;you!GRkiijDkgb-Fq?kJ%^Lk zp-%x!-__D(TtKFzYPW>c5}RD_=39`eh3Dt1?7f z>Z-WQeY8lb<(TGedA%I-)xPz$v5u)bklk-1#1|rpJ#ML^zBuHUU0dJ8uDlf?_>97^ zR1fX^;8WpJVnSf2XBAeoD?3~XFgs4yZIIA~s@vMylnVtn&T(jldcUAE93RyPf}>mv z-?OvXAok~O$KxVWkyrtHYX7YqM#GlAp78DTMX*qX@7;(E(&0-mvTp$uneL0J$;rqd zP7%H?b2qE%)gIh(8)=c)Fag|1$(wj^*-OBFkm zx$!=m`94e{D?1BXgtystR3rG0xfrln`$aWyGq|ja8bO#nhbv;9E~nZ0`SHg6$%YQC z^32D3{90HHwZ4L~@6ZN&`I`het4ECBPq{fe!ZC=u1PNX4)&OSI+jx8;oSX40Po@b;I0hA8#3Q4SI+v$&*3=l@WIeoydDmc zY+$IVbgUNQvh|^M(P6+f?{E_^ua%anWaxLDmrP{qDAta8@(GC%wPfw4;NPwJUyvmD zS+41~S+7S5U39Zl$4I)nALjX=4-BmACWzf^_+Ri1u6eC~I?QobE^NE1YM5+)Jg$tu zvC;z^@k6fvajL~s^S1tRO0$F5=|bw~fD@?MDcJ7Q>csi!0y!|+%>ym3YFhBaigE3a zu!g#-TbefQ{*X%Av{O{}STKAVI}yrVxN=er(Zmeex#@NtVSYE+v5=3e?k9m-?Wv{J zfIzF%RIP8(d4{wJrk({m9O|3{wp z8f4m^AZ@=$v$L1){E0*#09%(~j8jWxQB}%Rq%X{OT<$2tBL|m(ag4w{85e(y#1Xz> zAx*lNMZF8QH5Sv_7xZY$#RA^PAaDD!guLCGTCN4I6oaT?)nCFvXvg5dwk~g+y#zg;B z3h|%mbzY$B1Rbm7U3V@&Ja`A*hlzE+tgQQ=KmP)BRkCrxF(3Xv3!oCXvXR+!(Y=ST z=(OBu@hY2|(5*@^9kUpRD@DekSkvgI3BEp^9Cdi~E<{PAIF+46 zX3b83kISlQa#U?8dKRibAy>{gaX9jxuCUkHt~!&kYRr=s|Y{o zhdJIGgP3FyT%!17$`G?>CYv0eA4fo`GG3+1XKZzotJ!UR^awxb{+U8XoEIDqy!7wQ z<|qu1Dd4bv-}p&HXV>&lz3Hc&3fR1$5WY{z|F;(y{Nz31RhnO9wl+OzEsX_i{%qcf zT_q)8$Vjp;V^`ayO}@(8n8oZq!f+D!}#){*Jw~fb&^JS@~Sd#S{VtL`=>i z=dDrId3oLED<*u|6CSC#L61iomD>4%x6694QoA;nvO8c}r&OrfYyNK*plrOGWb?MM zd$~&QG57QP2@KM&s68FSglY60*+W!Vh)&QX!sH(*6be<9-lc1a4J=!fqH*h7vGw=&IJ-?6{KRtf`-jo12$x&+AJmVF42_5RW$h|P)3%tS(z6YRThhY}@ zmEHH59L(G6?ME;^hj;v<-yiStZc)598$5yU{mXXG*SKLhCpQ?ap%|f|r zn%7<3ziJ2tcF(1@BD%J1Gm1>^XT<)~=f-j3DZZ8dWyEzk3niC5|gSjxIV58Jbw3YyFlF#smW|8;wJ-}R0=b}X;7xif^crwg%3!ku{2(=#+J ztX(fwWodUGMu-QSU2fJswrmDp@x*6mh#n=`yj+!J2kdCXDeGx<2{W}B35qnC*|-13 z?74K-d7U@dT(jj}FMKy2j1ti->qQw-H#6H zwsCS((zJ;fs{8|4nsltQkGYwd_35wLH?`xZxGpiexxwOQ+Bgq3?^nD{?*FAvjXU;i z51JST{j_?*K7F1c>VCH2@ca4iUk#aaH}Zhs4;HbTpZNSatqzNxLSAdUfTGxb-!(n> zbhIa^KiePOJzL=UI9XxCL@TX!aN}Qz_w!ZyVvfhvrNT(pD{e|U!n7mwb<6+tyYoej zr-M#dz{GjS-PQ}a$@8J}y8B@bzQ8*%QugkbCg%;;#k@eFJCoZ2>;&ZgyY9O# zAYzO~^*;kZqMm>o7Ly+5p&DiHoZff|X^QMe@=W3IpKX!_e=WDB*e45r1!=pu=i=#jj_J4MS;y1?_USkRTK^SM^7|Jj6qp^)Kkqn9r%KUxKagjhsOHme>a2SCsD0QN`+OP8KM2dtQbL; zv-w7s`<0Jc8(0!4X|5eS&2Gg+Q4Q`5fE=L!!_iK=CL?Rz9l$qKZq@NBnr0Xn>aH3m zIf^76`u1!R!HGTWrF2x^-jX@tS?r3v&huRa9SS)=F7tJGI~p_uDXHx*XaXp=@E@-) z*uixU7cV`2&nNIl>300`C<5z#V+)^FHvq=3Cz&gk{K%3+(Cu{~%ALps(iFqDde*4x zSRnc-C>m!a!{waImMV^OaZ(#-Az2W}QrXl=7iZ)MK?L}|moQ$XvFu3t-*q0Y2j??H zixd&=L`%QWltd@n23 zjn(VpU`69}Ayy|amI6%LpRKu5T<_)wc(1qf#nb5+-JGce{EPKPg8#AfG;Kw((c)}U z85sOmQE4|2svw9knDvQ8ujiw7m9eMG-MP){rTd-m(~;Qy1D`sgv;Vb0&A&fG)u#Sf zrv`y*$M&w54LU)O&KuXi@IZXR?+D~pa)0N!PSQj`O9o5db#E6%i3Ny68H^({CG4P!&>@<6fi<7q`QNZx@( zk8}U#zXx7xo`*2yQv8TB)_FUwx~kvUC{X`AA`JgP4Q+s7xTdL%>be;yfbIW0(c-xL zeRm^8MRZxeevoN@(NX&~x78}o3$&L*5{C>GI{Z5%`jRB}Fe{d4(i*tg&6bj4A^zLI z0q}i@dG0efx9uO;2w&R;KYk-x*lIFx?($j$GV+TJKR{kmtIxMiLdF2jJruU*jaTz9 z9zV-ZQ-@i0Igxd9u=&5t<~;-|&1!P!;wR7e)+>tMp&@~Zf*3mBa`1mC0b9H(>^?Z33 zG_9&;&4*5hPpxnb=nTFw0>7VD4iZi~c#PM$uTLnW@G$e<5!C`XkwPa&zbzjLKV?p} zAFpm23v&j~a=bK@hwd&?ETR^c8sG2z`e)83FLphmB60|<-W*92RU3a-KeU4FnLN&< z8$TTP1OT*3xdpVrz|B`+FVFXKvvZ%r_(9&JLZrjyzt$wn)JT=`l+$KMY@(jbbhPel z+lTB$&aa=hp3O?yf0P@YoTe>4))p>D2O7OogT_i$xTVVWN5`#gBfAuOP@i}KFL)b)td>T^ZK zy!{P8q*dix>Ut^(>XAkDJ9nS#0JL3W7SjEk-B(SuqL%~sco7eFN@!C~XrVSLLRzx4 zr?*KJe^Z7zp|nu=sa0WtAvAd;?>G!hYJu#o-^sBTFAwDKWfaK4uRqE1e6GG5x2&Hu zme>rED7d#dHt2biKTU6!o(FFoJ_I-c*w5|@gbyvNg{jsJdV{(+h*>3l%<~KaV(;xh zU)?C-iQ(lTl<`QcV9{f1o*=Nu-2>4^`xDCBg^OU91ncE?>*0*UU;9tr7 z=joxLhJ7}h+&fK+Bra>FtdcU~U8f!!Aw5j|uZ~JbbT|XRATC->-SZPi&?9C~@XcCD!{#GOuzd8&8Q<%_ z6|sMPY?~8lqpQv5Sy?L{qvfmq&wlbElZlH7DwLR_dJ&{M#2QA@t~fS|yX4Nnvu%fl z+yd?!n89}snVFphv%?943Ml~L=vDY^ZkcED{fU6dB{!@nA2{1MbzaJ>=9Ot$r_eGH zys@yba4-o9!x+KgloSm34f@+J$3u4ad_5%=?B=E5YkfX`J?Ver>a&0t)6GOpFhc)$f|8}DA}zWk2X-vFqaCf@#Hi~3YCwHSl{ zQA^H~YupAMF~V>FTjX}B+~(Z8SCiT$)m92E1klFB6v+HIE*@yY-p>1mTS|W5wjZHi zD$KK9C7?@sU?eL&eD?$^7=cM-6-jBqkK(9>AA_l5#dPC$OAj~?ec55`+WDD zpb~s*5%X&t5-~t__*GT++uGtss%$o}XaGPQ0*8P4)=PtZvHsnHh>F>U@}v}z6ABmN z-iKqJ0kU9H;ZzkmAy_$UPtn3#fZu|t-F8VW&i9tqh~(0aW1paw0vBxaqU?P_GDNP; z&W7vP`>x&z>#%7J0@hz(D}jF083_<4&Wv@^8ApQX=+l1XcTXA7-3FCFVVzrsvVppr4WhxC12CppRB>Kt;4~QSq%Q;reeWl&%Cp z!M~HDrrtjph699_<{CHLQAydK=j@)A=f<;=INQ;XRS*b*{8iVM6Ls4yJiK@3Gs?U704xeb z8LuHB^Ne42WxDh`CW3n)dPfqZ%iY-W9r34bM5sUr5%G?~mYJ!HaY`-qRmcHaA-5JL zNE~O+t+bKYipfx5^0o;jO(&e7ftL>lax|ZWvebUPiC&0AQVgQDlzhmW0YOtZ zX1oj+EQl3^5P|mHxF8WRNd>Irk_KC>(_s-yW7g{GIvk-c)c*q_DrgzZWuHeX1LfNG z(S%?*>@nm|`2D*aYH?a;zBa>iu&}_@)Wbvt9Wz$M(P6wZCH~5XN*n93|I{`0bv*Cu9?T6FmouwFrjm0m0WMbWG7-NKu z>;LI%KPtQ!I?GP^n8_clt${%quwS;kG5dHDWwRIM6~b1MH^>OZQss$J)N**sXY|g8 zGTnc5+8&>~i?FE%I5EN#D=J`CbaM4CQ48N*+apZKaPvS{0Yk2=CvcekrqIk1rpm0> z3q5sxGgg!%?#5sA<#Ft+{H%(5da{zltdkFA(7;@ks$(1SIs2OKwU0aTqVfC#Y^n5- zX2bd}(C>Cx_2WA_9u95a9E^VhC@#1)L)Z+h!LY0v?@AG|?Vxyml$k-Q`dEDtFL{CV z3p4+?`gpRg&qa9&`fR>`>CyoL+5YU7gwLl;Q@M@h zolw`(S}?|!NTy9XbX+fNrg}sLBZ?H2Ic*fDl14VSw*KC$cLg82b^ma2E{uKb=IHkp z#~J=)n-$3w|IHR*^QqF{%D?zCGdeBnSn?7)!}Hn*iG`*O3aHLHc*}1LTVj@X--1bi zPif4O`&I9+p7Y4ANQL=+&Z|jO*n6OHdFBNdfX8X*@}d@=%O{yv($JI?B^3# zo1x~ODvzx=geT6$sSz;6m}ixwcD$Mp?N|@eO5FtJqf3#nq>o9_)Hh#-3t*4#SYf&r z{wiBMN#=nw4GepDy(hhSOB@LYnh`aW)s!3C&tzfjR*M?;Tvwa#KJ*0&iopN1#O{TG z6w9GTk)G#@buSDvjKh433@%J6fF_;>-% zHm;!{$DvfqtAs`cvjXs??o%Q^8ddYvYux8qQIle7bm+mISAs2Xz>% z#18oCz^=55$Y<$j=32W|uO@xAx3S$9EHU3s?7>6aFxyqHE@N*a0VNGB8azH(Cl0yf zUwIA7j+OJtoSExR-j0q{#n z`9>>u_0U;lLmU|)(RVFOB?T@mR+rR=PP`o{RCe;PhBp$S(K(%YZv~HDj1CYC%v>?MeIGm0gxLGoj!cgO&a6i;kLAM}AMEsl#i#tWgS6 zVwsM4EiI84>MzYMe}AhPEz*$}OWg^pnES|c*3-%{r;X13p|TopSv@;nYin3{T5V=? z&alfm^IC1r)$aIY05dy!L+_deA_6YHpuh=|sFM1`bP`BfW0w;b?8So& zp+p6!@Hyc*?jUxu+kL%MZsQ)>&_RzupMtPpK#=$+7W<_-JV@%cf6Q7cv6M z(U@;_q|LGJ$CypBz4BC!-zQzmY!~7ZM`>=!o27T2Gy}v zt~;TKe`So>g(Q=aDD1xjVzV$M_QJ>~lzz_tROmm=TUOKHD;G~(CqJ#i+*I>WsKP6l z0lz3W$)qoE{nN)ZW@nr1=RMc;tF;*kXnB*Y^nkmK`p-5r1_)3g5dF<`ZGz&(lZna6 zmFCFMhO#(}HnuZdGc=s!78Q#g4QQ|OaE8LWG;WP3_-fI?^$8Lfo( zo%m0l#LIbQm>in2W(a7*@4Gn|s%gTl8bsf*gA!$xP}Qonc`~8UlNvJcVQ=QJwY$WJ zK`#h?q?2D>0*-d{Xxp@>q>HLnwRSa;!cWMTu<0TARoLvs0|^gJMlb4YxRs;O7jvaq z!9zrg4{jhYkd%K5z9)p3%c437@-)#mF6^QTHG5a~=Ec!Vs6clPs4#10^MuaJsM@K&`&`a!|x`AL+LbB}Gz3v1cnwisP!sU8c z?b;nrJc*k@&(KWx(jn<%o8r6|pvHai(I4 zE+f8##MC085lvR;Jfw)%%uaDgKJDuTIx`Nn8V(P=Ok|7j^MCH?9SF>9!>x^V-w3FO z&CYomg=!s%4)1V;Pw(wnW|eS^y|Q$%?oFsXdm;rjO+vk_-9G-34a) zY~{$R(OP)6*Q5^(k>EG!$GLMVrQoD-mDtHN8_Gn7o7ob}o!S)re-_{^$aX~@M`4uy zh#I#ZWY?r0X|@FDq>PKz%NI}Z z>`_7W%T=ei1*yP|z>sWhRLz*#=b8hFXGx3sI}7T)gNJ^xvI4GjF*oom&ZY{+7gqSy zE~fm6${EY>KZayZV*|`x?)Xo7fcPn!m;yWOgN7E)4_>Hw0jk?H%^{RqTFc-}yI4Jm zRQ(;fUDkC5x4^c@`z0oF+Rs-;h;gk|}-!PJMK+{ewWBc`M zQ+{T6eIZ=ukmdir1=`d#fiwchNPxs5Z8Y4Bcr11-tHi3|0TS>3T!RB{{eaq$S#|MH zK%p}@n8N9mdCrw0+ zF0t#rIK1}NBbjyNr!y{0<&KaLmmv+@q@bWMF*bfjMwU`QB9u=*V z^Y>G`H8erx-gw%c--w~ITtBO>*y4Itqam`!|2d?D_l<>})yHhB0UdRxnz+B=i8UuT zeu3uZ)Y%iaej-c_px*i_i2~ErTRgZ*PpiP&XSG|MBzBj$fUnzVL)?d6U#)_EmRC!wdhtXqIiqAGLsg61LRw2}Pztck z?7!J}^R+8y4tL3MELQ*WZGcy4^7DUpd(GB&i=o?#&DY1po|o@K?^4cTRffwg&iTB0 zR$Ac|6}`xhoJtx;MRN7rKnPArNomrro;`xZyPqy`Elv`+J;)RmR51Ub#^?{~Nx*>~ zQ@OVpmd@4#;6x*%#i#YWP#55~wr+4uMHWvSBZR#rDlN4MrA$m}ncrj=8BFp^ohq_x z4^qd~7%l#zt<_~*%Yc^VfeyG)maner50RQb(MZWqy+PDn9CzyQ-e^0dGvnqHY;gIj zOQn+`=632?8@T=wh?3Y_b2gnJdNYtRlcwKQ)OP`iKlF`|_{_!6$2afDUtF)RuV4Sk zz^(A4oqHSz&wm+;b1&L!Jf?s|q4=5OL=#9#Fvu0)E`0OsvSz|lZq@XWcNOVpPi|1DqOizkJdNz@)YQY(^F~@NEOza(yo5w%dC3{Z1n)Yx0CHSQ z)%ewwTh|5(`c0yOT!hGKc%@?s^HteG0N?tg>3;#+ zBqZC$cx9s7?aBe#<#PG7(@yJlyS8l`hSBMCk|a5L^yszMUVGrc0Zr42#iFk3{eE8- zF3ALjR7dhpS<~3-_1^K0cgRrN%*;%q(O6tu{PZV3wdcWyOx>88p3zmU-D>HYh7l5+ zx0|id4~qH8{rjG>O{-e1Zr!@o&J+s8iSK^*yXA7ZR;y()nX9k9T1GNPcW!iOq=+4v z_|iHZmcpz-ERSCzB+RR{fSub20!Xw;?St`MpsT z&CbqNtJMb{cp#gdnw;Em_Sxs$dh4yHo_gx7x86EFzHM^ylze_>_wL<7h;FxwF`k>7 zlMFdGH&-riKJ&~o=jZ1ejmG7dUp_f`%8ngpnP$0CnU3T5@ZrN}pMCbGO*;#PiJNb} zISj*0CUfAx0qJN-5{i7GOImiN~Mpb^8+&6P-@ys;jPg?6Jo( znM|ovVvIE!jU-7FMY;U)%O84hkCSoxJs#OQegDv*LlYC*U3YSPeA_Lz+#=a5m&+wdGB-E3 z-uG^R6&YiGufKI}vx0E9)vhmBbB=rD;E{j%=WiWdSQJXU@6g`K>AATrrcYbh-1ouj zKeV_|Q;>4WxflN6#+zcsQY0jqI7SpPf)E*o#U)Ql_v7TL#gwtG*W0pXnqblFbeC$? zoNFFAwD0p@{GXjLUS3*UT$taw{q*w0?2mr*(}}SOL$&MGTBTB6IJStHsVVth&(CCR zMMZHOsfyliv=o90xjavlM$J!RK60ly=Ms0Ab7~mc!Gi~Lx!f;)@ry%;9&NP_fBVMo zec}_Jd~nYLxBuiv?e6?i^}xON-Ff}>A7l(T7mA7xAK4#9{ZD=R<3GLQ$AP!}&;Rs| zOD?%GNs=G` zE~OOX72?Q^0l<2h11vyA1ToZU`ki(#IWw!-=EBlaHk<9WJ=@480qu4A7@&&RUB&LwL1Ou3X}L5I_WDnx-^OfAi(9xOMjrbVL@8E}U}eIX}GZc2K%| z9=+o^&$|fWVy{R4=#SrCtZd!7ZQDJ+c+l^{um9RB7LP1vjqD%1>CL6`w$pa(+PY;& zyGAW_?9-q4;&^52yyv!`pRw(C#2bjz(eifYH6+I0HP@7?xeg6ZCe?mh4MFQ8yu z`Qn%Nn*FFBU2@T-rQ-G-JI*__zwKI^y4~p1UAxAor#XtFi2lt-KYr@=?fLxV4|o4U z$76_Tgs3YyJ%WgW0STgD*Ub<@-uAY4Fba&~Bo>aFpO~rq#dUucrHMdltn zzEFzeFki@6Ci;Ir@a~zZO1ItFG`;Ql&pj6~iqnWCp|0VZZu#DoSH2{xY8j+)L42Q1 zRW?26tmj$Tf-v=C)dkhk1R4}H%R?_WwqEHE5@2worclU?d%o~u){6qS?6x``#7&4) z9IMy9?a#+6r_OCT^^SY)=P3J%7r&uBA7@nUs{i)t$-?F@^zQF`mn0vF0r&cK?)MFYbuxmP6?(oiTjSLxW(N0&;JdgjsMA?UPnN<8d^GfaI7+G zx~1#hd7X*~aM5hG3*)n#7`h6E0d23NJN9}`>Kd%Lo^*FI8A>TjsivrNGqVSu+TW}< z9^Je5^{@Y(eFyfby7jg-#+%(<5QEH9=zwV!-tm(r3u3_ zyIxnyf&mWdpiT+{K+erssuA}C5mSMP?YL2rB1DoP4tqgF)A7k^p9UO*t>d98K@yOo2fAoicuz%kZ#j(nRk32Rx zIcXWD>lD(MeeQENyzaHX)9XB5T|AJ_jn|g@6BCnrAHJs>F8M)^VD-0u`!x?e^vnD2 zx&5!-|KWDShr!Il)g?F_fbZYGzcM-5Y&K29cAV^4XFlgMpZVN(zI~IXsenbCB+ohT z{I7iZ>tFrHZ`gLW*YglUoKsE1;}fNRw_BQ+_{r@*dFxxR$(M=~6P29fKDF=3AN}aI z-~HX!+PeAWfB0W-c+;CRZsFAJr+xZUpP?L(qHf*1t=6b-h&TwiA_R+)C`f23E`7n} zn`TeX7dD@H_Id3_+t!>feC`YH_@lSC+xzdh>$d;#u}}2+0Wid4{-NtXc=3goG+X=Y zONVp098Dn#&?(z@EgpMf_YeN{;~)QUzuR(++};QFKe6x7DW~q>09+8pgl1ZWLZR7g z0vdwqB0g+TMo0i>0U(~wmTV)xv{)BHSeEYfy0&2zvW1&(y6LsAxysG%IQyLQKlZV| zGtInV+WlVd%$+;G@lXHw<*$BKAT^V5TdmfG7d-#dpZV;+eDlW1O|#uj3jw#COrza; z=A9+$vSO#xiQ`!2!MAMLvTxtMMyvVFfB(j=v!)TI^(Ez(cOBlbV@{yzm%sMum%scq z#r!EdcUk6 z)X-IoAx&f3kTvydG;=I3FJX)v*A1gcR;L_4K76#c_<`%+`>wxwryq2?-6jx7SV|OB ztJMHNX4~Zh39qz~#ry46n8esF1XP?pH-7lZJqzvRA8*`!?kRINik~>N^l#t4?TQy% zOcKAgTy1u}@85j$)o=ZSPQT|zzU|tRvlH!JM_{zHxR5UvbxWU_n;D;;upLLYbVVcU zb$)E!y0x~nBm)Lf6g~X#!|!;e3V`^{H+^rZcBtOi|KyVo7Yezh<@v9Cl_N!li@ZiBX?HP`+9Hm^DeBp~;W|`T!x!HbD zk7zfS%cTimq{vDp&+BC}mgn{A_4@Sm^obi8G@a>|VHov#-Shh8N(lhcl$xd~OYJ}R zxzF8j!^hI}z=uD4{U81DHHNACzHeF9!+RgT>Z+@*zUs}oN;v1Po4xCPUZ#0|DC<*J;_h@I6}zsB^-R}`rpnnXUwGMfesKHw z7r$_BvUKXE$$$FyZ(s6jS5A(VVy{&hFPF#003Zx~N)yYn1kfPtsVY$vVwgI@qVIKG zH`D2KG+p5$P3h{$EnfXV#(2{-l_g3-u$h?|Ay}bMICkuqX_}NKGcz-`Z3`jx?R(_lcr#Auhf2(xBhtoEe5iDev`qon{L^@(|i8vub*?yg{^jNY}|R#6_?)szn}+j+ zFMjUW{J|*pE!#MK$7y%oc_(9{)ARejhZMpD^P`~C>1^6O*Yo;u5&{U@Hgc|AU0xg? zE7|4%_jUvdK5}B^*vgbsCRnRcvu$&Bc3RW4@}?=>GQGf86s1@w#Zh|JIcK)J?KqAm zXXjMSFmx+SeWId%;BPBdv)hl#4{3W!&|@@4OP|A#Jm!R18IxtYpGKKeJdlM8~Lu3`>sVQKNS9jD)S z-+hHb;YYXsaDL%Xr(2I=Po^9wrG}vZVzzCoDoJUGFkPqZ10%r2Vi5q|^{#i7N(D;k zp+g5{DCIMs`7BLiM(M#r`zcKj5|fjYzyJHczyGNN2$9c!{_}5q;~QV~s#n!&Rns)` zZZ1vJfB5qM`mNvke`H*=)~KF)-nmaa@mMjRKXl}9v)$aXY0l7e+q62ZwT}4uOCZop zBU{KS3NRKU%#tJ`gp`VTDp;C^=bwMx&wqY*nx<1zlOO)zUpE>xFYqKs*dX)3h(X_~QNh_p7Q}E|(vA@Zl&*mY0{gNU}NmeeZiO=e*XaCyeSU$>y9J zZup!G)!ud2oy9^f>t>dhm!l+}n3&j*s9LuMRc4c9`E?p6mSt_DOL##r=;y{P^RK&y*^&(-U5lUUbprdk;Sl#a&0sD!O7gX3Tm#5~&1D;;G}B zPQG%@+y3~dk)3^|gMs0S}Cl_bMIWhd;VC zO;ZA>-ENPUim|`;NYnLJtJNyxN{5#h1WTVl{!+GFN|M;L-0`XDfBv_x zpLfCYm;3df{qkqqwr&mrPx=?qQef$5g!6IgXK``q<*)edrw+At?mX=m_y2Tk+=-LE zZrYlm0l+6d`L`Fn;L_&RkS)NM{$Y34wa++n=hXCe5d#O)rFCQ(cfZ#C=X!kE|fDw{Vi9@=l@%_sLC{rKP9@bUlt$zs01so!cJ znVG4Cp&y0Ju_n?4xuv4luPrP;J~KP1XhTMQ;jGoXDTKNU=`umi%d6@ zV9^dd%Gg-0Xff34byG`8w3PA4#8yR{T|T^!&5A_vA~hv?f%;|PB?g_hvZPHkgbh7n zrmD3gZ@g4)ADnlK*`^^lL8upE3;|-4(u(P-nD|kM6e5$kd-m)(m+iPGYW`G5ECdaqcYdyoz zycg2gZIJn%76S(8YKUNJbWic1&rpO8^NadModNlI%= zeMJ@1)6+>D4^asL3E;2-*H0-$gdj}`qX=|GL3m<%;@IIMHYmAbp&GOko;t3LD4Jh5 zTFB*y9;FeFd1|Rbuq0u@#LQHyb|h7Jd92XxEi)Qh8FOK2zEW_5I1-AIB-B8{vTROc ziZBq|?e&z6b;sl7Ml)lZJod3>CP*)imud@#G;p!yfM!P*u|Jcgy8cngj|_!!SaKIF6$bBMg1b)Z_s6g{8$z#wz6t zaU5fv_@O7lSkVn2h!Dlq)Mm4(8LDY$BHaMSj{tCU*?!OO^jk5bnT&-5XsS`K*K@gS zr`0y8nJwgf-%ka%Et65kSsbLXY8pFsp7qp$SXbfJTW;qf&bW3;sj8|eV+!J$p$4fB zm6C$8jiIqd-ak#0e!>t=6l6RYm8Tfv0C@>y4IUl#(!}RMvN3 z1_*I(+PVU!iddu7vNJ9rib7C6pRd(whG7_*5r%yM+|A|%ur!QpP2>ET(r2I**Xxr$ zq32rRLI>;%wF&@`P64+#P634&_W*#<0^%xy6cELNGYk}C1}fq@2n7R2z%U8{r4kMk zxH>eFsUR_ccy#X*yLO$)KtyqD*cRs;aiIW0Y^8-(j%Alzsxbh7SOH2X03%#gQ(NHx zDC8tg6e@%cET?eLMUUe+CaM6eF+czm1L2@E-~he`0u9FNNr-5L0~P=T%c=t*#G{Ws zde&KI5kjQny2hL-F5D{=dS6V zVF8%q;T`0wr{54U0(!&}l9VG30uwIb3^+#=5EBSMs36nfHn!#$+pA(PB z3WBA86=XIk@)QYzi#P`$LJ$e$ioh_81duE)lZBV2Y0B=~wa9@5!T?-g3IJ#bd5nN_ z#D@T~oP#29fdLp`4gQD(000R}Nklz03iUx1kR6Bm9y1VPC3A=dL&%1>bsPZa@-9LslgR*p5QuZ&oXKWI02s0p z0~kmF!I2!m2EbNFU&Y`HhO{5ODu_V9K>!HC1t7r?1Hc3r2*d;k1Xu_x1j2%YkhQzA zW>^MnIp@Cbuc;L!Hv__ai0(QtjsO4)4irEEG1xg8jFH3~agb*W5C~n2F?*i6bVic2Fw{pu%ZL801h0`NJ(-G zcp&wM&@~)D03I6H42F_m1m-_dr=yWoFa+KlA$1CX5t^;+c9)gtvPEl60?8Q+0tg_C zIRk(cDFOiz3;}RZ0C6lh0xkv|vicT`;JSzM__#kgARfnzbDktgr_(VELy88;1}AJ~ zHEmZAxJS2U)q@p8yul~XaklJ$K7vc!ZoGw|uA&nGiv}YG5D0a!Nt6x|+0j4^AO?u_JzsES8U|y`Ftkw-|C$iVBeZ)!gTceY zz2OyV4Bjx%ff(eN06c_F=K!i0qFoPd*TM7wBm_o$O{6v1IfMsuPWZ{NZPtKql8BT@ z03ggwjyT{L5Cee$$k4<}0I}hR2lg~yjSIXoyI0=rA(9dTDIJ$<)i?|xin)RVi%`aF zgi(|#2nz%W;aGr90RTc3I1*S$+O+hvX%g!BXot_*U@AnTJI3WAZrRaRxsYU`zR^*5>1Ojn^ z{=i>_k{xpl>-M7V-pK*@*V~rW0Qdj0E$R;9tyxdaXG>|4~bv`tqpqU zIF1Z`kGS#K0re9Aj0NT*7HA|&N7Fqx83q6bqz$$Rw zq1T(HilR+TO&NwEVM=5p^;%j;iaBMdJ%%WTgY&2Wz{+$QB#Q?KVJbtI5{AWq6@3km zz)nN}aG+uV*r1#UPBAddHAR;T$mAiVj)zWAVV%MI6hJ zF07VKtH4pk=(B_SDY0z~BU308Bmk7O-X(ob#)_Crkv?KYEC6#wPGMwNj$q;{2M%gP zOeDC?2CPVtip0iPQATTIGDGfd4OU!JAFd)LprH#3fRRwd(?(US)GLn`2M`$OkbVk) z;Ar$Iv?|1aH3cKiRjgt~N0gWE9xM@K^ufI%Pu zWEgJ{!$Sr0}hlZI{+X7EUMM&y12C~ z^JgRlKnOhaUj|}vXyc%vKE%MymB~L?#*%Pq4I#r33nKP z1IK_s0Am~k#{^>qVJ?7i$uT33MXD7Ug2fKp*JDvEJOB400_(h4f|9$tTN-JK9s%uG6G>323~if z4wn?bD^4i~V1w{GQU*aw#D@FAGNl1R0U*QR4O^9ck=mXC3`Ce^4G_wu(Jmt*2(5XC zhrfCBKYAKH2FGbI*-FS03Lql_z=r{07>yNUpm2l{a3b>p>wPJ#;QfxsxM6hg(WQ=$ z0i5vis$KArPk7wN2O@CQzbh7qjQz+djxtCv&@Bi+<}DCsXs`z)R;noY%E?=GH5s!# z?i3FEaRKGPXh|9gg}6g!ltWi(c@z_gC}7Xwbx8SjA9J`VT$Ex z#KFfu&sX`70EiR8VKhsVVHgHoD?L0q{^pkNkOzq~C<-z^E2zmH9TJ z(uvRf$5L#qHK6lx6CYN^rC=bE_-g7gq+r1bPyiu4Bv|23V4#fteCFu*&zcqgYyH>y dul1{3{~IZ*e^mA3a+m-B002ovPDHLkV1hb6!g~M! literal 0 HcmV?d00001 diff --git a/src/main/omr/glyph/ui/package.html b/src/main/omr/glyph/ui/package.html new file mode 100644 index 0000000..3391762 --- /dev/null +++ b/src/main/omr/glyph/ui/package.html @@ -0,0 +1,129 @@ + + + + + + Package omr.glyph.ui + + + + +

+ Package dedicated to all user interfaces that deal with glyphs. + There are only a few top-level classes, each one using dedicated + companion classes: +

+
    +
  • + GlyphBoard, a general glyph board + used by all steps dealing with glyphs +
  • +
  • +
      +
    • + + SpinnerGlyphModel, + a spinner data model, specifically designed for glyph + ids +
    • +
    +
  • +
  • + GlyphLagView, a general view of + a GlyphLag, used by steps dealing with glyphs +
  • +
  • + GlyphRepository, the unique + access to recorded glyphs descriptions +
  • + +
  • + GlyphTrainer, a comprehensive + user interface to train and validate the neural network + evaluator +
  • +
  • +
      +
    • + NetworkPanel, the panel + dedicated to training the neural network evaluator +
    • +
    • + + SelectionPanel, the + user interface to select glyph population out of the + repository +
    • +
    • + TrainingPanel, a + general panel for evaluator training, subclassed by + NetworkPanel +
    • +
    • + ValidationPanel, the + user interface to validate the evaluator against a + glyph population +
    • +
    + +
  • +
  • + GlyphVerifier, a user + interface dedicated to visual verification of recorded glyphs + and their assigned shape +
  • +
  • +
      +
    • + GlyphBrowser, a user + panel to navigate through the selected glyphs +
    • + +
    +
  • +
  • + ShapeColorChooser, the + user interface to assign a default color to a shape, or a range + of shapes +
  • +
  • + SymbolsEditor The glyph user + interface for dealing with glyphs in sheets +
  • + +
  • +
      +
    • + EvaluationBoard, a + board for evaluating and assigning shapes to glyphs +
    • +
    • + GlyphMenu, the popup menu + for SymbolsEditor +
    • +
    • + + ShapeFocusBoard, a + board dedicated to focus on chosen shapes +
    • +
    • + SymbolGlyphBoard, a + specific GlyphBoard meant for symbol glyphs +
    • +
    +
  • +
+

+ + Glyph Controller and Model:
+ + +

+ + diff --git a/src/main/omr/glyph/ui/panel/GlyphTrainer.java b/src/main/omr/glyph/ui/panel/GlyphTrainer.java new file mode 100644 index 0000000..886b165 --- /dev/null +++ b/src/main/omr/glyph/ui/panel/GlyphTrainer.java @@ -0,0 +1,310 @@ +//----------------------------------------------------------------------------// +// // +// G l y p h T r a i n e r // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.glyph.ui.panel; + +import omr.constant.ConstantManager; + +import omr.glyph.GlyphNetwork; + +import omr.ui.MainGui; +import omr.ui.util.Panel; +import omr.ui.util.UILookAndFeel; + +import com.jgoodies.forms.builder.PanelBuilder; +import com.jgoodies.forms.layout.CellConstraints; +import com.jgoodies.forms.layout.FormLayout; + +import org.jdesktop.application.ResourceMap; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.awt.event.WindowAdapter; +import java.awt.event.WindowEvent; +import java.util.Observable; + +import javax.swing.JFrame; +import javax.swing.JPanel; +import javax.swing.event.ChangeEvent; +import javax.swing.event.ChangeListener; + +/** + * Class {@code GlyphTrainer} handles a User Interface dedicated to the + * training and testing of a glyph evaluator. + * This class can be launched as a stand-alone program. + * + *

The frame is divided vertically in 4 parts: + *

    + *
  1. The selection in repository of known glyphs ({@link SelectionPanel}) + *
  2. The training of the neural network evaluator ({@link TrainingPanel}) + *
  3. The validation of the neural network evaluator ({@link ValidationPanel}) + *
  4. The training of the linear evaluator ({@link RegressionPanel}) + *
+ * + * @author Hervé Bitteur + */ +public class GlyphTrainer +{ + //~ Static fields/initializers --------------------------------------------- + + /** Usual logger utility */ + private static final Logger logger = LoggerFactory.getLogger( + GlyphTrainer.class); + + /** The single instance of this class */ + private static volatile GlyphTrainer INSTANCE; + + /** To differentiate the exit action, according to the launch context */ + private static boolean standAlone = false; + + /** Standard width for fields/buttons in DLUs */ + private static final String standardWidth = "50dlu"; + + /** An adapter trigerred on window closing */ + private static final WindowAdapter windowCloser = new WindowAdapter() + { + @Override + public void windowClosing (WindowEvent e) + { + // Store latest constant values + ConstantManager.getInstance() + .storeResource(); + + // That's all folks ! + System.exit(0); + } + }; + + //~ Instance fields -------------------------------------------------------- + /** Related frame */ + private final JFrame frame; + + /** Panel for selection in repository */ + private final SelectionPanel selectionPanel; + + /** Panel for Neural network training */ + private final NetworkPanel networkPanel; + + /** Panel for Neural network validation */ + private final ValidationPanel validationPanel; + + /** Panel for Regression training */ + private final TrainingPanel regressionPanel; + + /** Current task */ + private final Task task = new Task(); + + /** Frame title */ + private String frameTitle; + + //~ Constructors ----------------------------------------------------------- + //--------------// + // GlyphTrainer // + //--------------// + /** + * Create an instance of Glyph Trainer (there should be just one) + */ + private GlyphTrainer () + { + if (standAlone) { + // UI Look and Feel + UILookAndFeel.setUI(null); + } + + frame = new JFrame(); + frame.setName("trainerFrame"); + + // Listener on remaining error + ChangeListener errorListener = new ChangeListener() + { + @Override + public void stateChanged (ChangeEvent e) + { + frame.setTitle( + String.format( + "%.5f - %s", + networkPanel.getBestError(), + frameTitle)); + } + }; + + // Create the three companions + selectionPanel = new SelectionPanel(task, standardWidth); + networkPanel = new NetworkPanel( + task, + standardWidth, + errorListener, + selectionPanel); + selectionPanel.setTrainingPanel(networkPanel); + validationPanel = new ValidationPanel( + task, + standardWidth, + GlyphNetwork.getInstance(), + selectionPanel, + networkPanel); + regressionPanel = new RegressionPanel( + task, + standardWidth, + selectionPanel); + frame.add(createGlobalPanel()); + + // Initial state + task.setActivity(Task.Activity.INACTIVE); + + // Specific ending if stand alone + if (standAlone) { + frame.addWindowListener(windowCloser); + } + + // Resource injection + ResourceMap resource = MainGui.getInstance() + .getContext() + .getResourceMap(getClass()); + resource.injectComponents(frame); + frameTitle = frame.getTitle(); + } + + //~ Methods ---------------------------------------------------------------- + //--------// + // launch // + //--------// + /** + * (Re)activate the trainer tool + */ + public static void launch () + { + MainGui.getInstance() + .show(getInstance().frame); + } + + //------// + // main // + //------// + /** + * Just to allow stand-alone testing of this class + * + * @param args not used + */ + public static void main (String... args) + { + standAlone = true; + getInstance(); + } + + //-------------// + // getInstance // + //-------------// + private static GlyphTrainer getInstance () + { + if (INSTANCE == null) { + INSTANCE = new GlyphTrainer(); + } + + return INSTANCE; + } + + //-------------------// + // createGlobalPanel // + //-------------------// + private JPanel createGlobalPanel () + { + final String panelInterline = Panel.getPanelInterline(); + FormLayout layout = new FormLayout( + "pref", + "pref," + panelInterline + "," + "pref," + panelInterline + "," + + "pref," + panelInterline + "," + "pref"); + + CellConstraints cst = new CellConstraints(); + PanelBuilder builder = new PanelBuilder(layout, new Panel()); + builder.setDefaultDialogBorder(); + + int r = 1; // -------------------------------- + builder.add(selectionPanel.getComponent(), cst.xy(1, r)); + + r += 2; // -------------------------------- + builder.add(networkPanel.getComponent(), cst.xy(1, r)); + + r += 2; // -------------------------------- + builder.add(validationPanel.getComponent(), cst.xy(1, r)); + + r += 2; // -------------------------------- + builder.add(regressionPanel.getComponent(), cst.xy(1, r)); + + return builder.getPanel(); + } + + //~ Inner Classes ---------------------------------------------------------- + //------// + // Task // + //------// + /** + * Class {@code Task} handles which activity is currently being carried + * out, only one being current at any time. + */ + static class Task + extends Observable + { + //~ Enumerations ------------------------------------------------------- + + /** + * Enum {@code Activity} defines the possible activities in + * training. + */ + static enum Activity + { + //~ Enumeration constant initializers ------------------------------ + + /** No ongoing activity */ + INACTIVE, + /** Selecting + * glyph to build a population for training */ + SELECTING, + /** Using the + * population to train the evaluator */ + TRAINING; + + } + + //~ Instance fields ---------------------------------------------------- + /** Current activity */ + private Activity activity = Activity.INACTIVE; + + //~ Methods ------------------------------------------------------------ + //-------------// + // getActivity // + //-------------// + /** + * Report the current training activity + * + * @return current activity + */ + public Activity getActivity () + { + return activity; + } + + //-------------// + // setActivity // + //-------------// + /** + * Assign a new current activity and notify all observers + * + * @param activity + */ + public void setActivity (Activity activity) + { + this.activity = activity; + setChanged(); + notifyObservers(); + } + } +} diff --git a/src/main/omr/glyph/ui/panel/NetworkPanel.java b/src/main/omr/glyph/ui/panel/NetworkPanel.java new file mode 100644 index 0000000..8ff87eb --- /dev/null +++ b/src/main/omr/glyph/ui/panel/NetworkPanel.java @@ -0,0 +1,600 @@ +//----------------------------------------------------------------------------// +// // +// N e t w o r k P a n e l // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.glyph.ui.panel; + +import omr.glyph.EvaluationEngine; +import omr.glyph.GlyphNetwork; +import omr.glyph.ui.panel.TrainingPanel.DumpAction; + +import omr.math.NeuralNetwork; + +import omr.ui.field.LDoubleField; +import omr.ui.field.LIntegerField; +import omr.ui.field.LTextField; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.awt.event.ActionEvent; +import java.text.DateFormat; +import java.util.Date; +import java.util.Observable; + +import javax.swing.AbstractAction; +import javax.swing.JButton; +import javax.swing.JComponent; +import javax.swing.JOptionPane; +import javax.swing.KeyStroke; +import javax.swing.SwingUtilities; +import javax.swing.event.ChangeEvent; +import javax.swing.event.ChangeListener; + +/** + * Class {@code NetworkPanel} is the user interface that handles the + * training of the neural network engine. It is a dedicated companion of + * class {@link GlyphTrainer}. + * + * @author Hervé Bitteur + */ +class NetworkPanel + extends TrainingPanel +{ + //~ Static fields/initializers --------------------------------------------- + + /** Usual logger utility */ + private static final Logger logger = LoggerFactory.getLogger( + NetworkPanel.class); + + //~ Instance fields -------------------------------------------------------- + /** Best neural weights so far */ + private NeuralNetwork.Backup bestSnap; + + /** Last neural weights */ + private NeuralNetwork.Backup lastSnap; + + /** To display ETA as a date */ + private DateFormat dateFormat = DateFormat.getDateTimeInstance( + DateFormat.MEDIUM, // Date + DateFormat.MEDIUM); // Time + + /** Input field for Learning rate of the neural network */ + private LDoubleField learningRate = new LDoubleField( + "Learning Rate", + "Learning rate of the neural network", + "%.2f"); + + /** Input field for Momentum value of the neural network */ + private LDoubleField momentum = new LDoubleField( + "Momentum", + "Momentum value for the neural network", + "%.2f"); + + /** Output of Estimated time for end of training */ + private LTextField eta = new LTextField( + "ETA", + "Estimated time for end of training"); + + /** Input field for Maximum number of iterations to perform */ + private LIntegerField listEpochs = new LIntegerField( + "Epochs", + "Maximum number of iterations to perform"); + + /** Output for Index of best configuration so far */ + private LIntegerField bestIndex = new LIntegerField( + false, + "Best Index", + "Index of best configuration so far"); + + /** Output for Number of iterations performed so far */ + private LIntegerField trainIndex = new LIntegerField( + false, + "Last Index", + "Number of iterations performed so far"); + + /** Input field for Error threshold to stop learning */ + private LDoubleField maxError = new LDoubleField( + "Max Error", + "Error threshold to stop learning"); + + /** Output for Best recorded value of remaining error */ + private LDoubleField bestError = new LDoubleField( + false, + "Best Error", + "Best recorded value of remaining error"); + + /** Output for Last value of remaining error */ + private LDoubleField trainError = new LDoubleField( + false, + "Last Error", + "Last value of remaining error"); + + /** User action to pick the last weight */ + private LastAction lastAction = new LastAction(); + + /** User action to launch an incremental training */ + private NetworkTrainAction incrementalTrainAction; + + /** User action to pick the best recorded weights */ + private BestAction bestAction = new BestAction(); + + /** User action to gracefully stop the training */ + private StopAction stopAction = new StopAction(); + + /** Remaining error corresponding to best weights */ + private double bestMse; + + /** Potential listener on best error */ + private final ChangeListener errorListener; + + /** Event related to best error */ + private final ChangeEvent errorEvent; + + /** Remaining error corresponding to last run */ + private double lastMse; + + /** Training start time */ + private long startTime; + + //~ Constructors ----------------------------------------------------------- + //--------------// + // NetworkPanel // + //--------------// + /** + * Creates a new NetworkPanel object. + * + * + * @param task the current training activity + * @param standardWidth standard width for fields & buttons + * @param errorListener a listener on remaining error + * @param selectionPanel the panel for glyph repository + */ + public NetworkPanel (GlyphTrainer.Task task, + String standardWidth, + ChangeListener errorListener, + SelectionPanel selectionPanel) + { + super( + task, + standardWidth, + GlyphNetwork.getInstance(), + selectionPanel, + 6); + this.errorListener = errorListener; + task.addObserver(this); + + if (errorListener != null) { + errorEvent = new ChangeEvent(this); + } else { + errorEvent = null; + } + + eta.getField() + .setEditable(false); // ETA is just an output + + component.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT) + .put(KeyStroke.getKeyStroke("ENTER"), "readParams"); + component.getActionMap() + .put("readParams", new ParamAction()); + + trainAction = new NetworkTrainAction( + "Re-Train", + EvaluationEngine.StartingMode.SCRATCH, + /* confirmationRequired => */ true); + incrementalTrainAction = new NetworkTrainAction( + "Inc-Train", + EvaluationEngine.StartingMode.INCREMENTAL, + /* confirmationRequired => */ false); + + defineSpecificLayout(); + displayParams(); + } + + //~ Methods ---------------------------------------------------------------- + //------------// + // epochEnded // + //------------// + @Override + public void epochEnded (final int epochIndex, + final double mse) + { + // This part is run on trainer thread + final int index = epochIndex + 1; + lastMse = mse; + + boolean snap = false; + + if (mse < bestMse) { + bestMse = mse; + + // Take a snap + GlyphNetwork glyphNetwork = (GlyphNetwork) engine; + NeuralNetwork network = glyphNetwork.getNetwork(); + bestSnap = network.backup(); + snap = true; + + // Belt & suspenders: make a copy on disk! + glyphNetwork.marshal(); + } + + final boolean snapTaken = snap; + + SwingUtilities.invokeLater( + new Runnable() + { + // This part is run on swing thread + @Override + public void run () + { + // Update current values + trainIndex.setValue(index); + trainError.setValue(mse); + + // Update best values + if (snapTaken) { + bestIndex.setValue(index); + bestError.setValue(mse); + + if (errorListener != null) { + errorListener.stateChanged(errorEvent); + } + } + + // Update progress bar ? + progressBar.setValue(index); + + // Compute ETA + long sofar = System.currentTimeMillis() - startTime; + long total = (GlyphNetwork.getInstance() + .getListEpochs() * sofar) / index; + Date etaDate = new Date(startTime + total); + eta.setText(dateFormat.format(etaDate)); + + component.repaint(); + } + }); + } + + //--------------// + // getBestError // + //--------------// + /** + * Report the best remaining error so far + * + * @return the best error so far + */ + public double getBestError () + { + return bestMse; + } + + //-----------------// + // trainingStarted // + //-----------------// + @Override + public void trainingStarted (final int epochIndex, + final double mse) + { + // This part is run on trainer thread + final int index = epochIndex + 1; + NeuralNetwork network = ((GlyphNetwork) engine).getNetwork(); + bestSnap = network.backup(); + bestMse = mse; + + SwingUtilities.invokeLater( + new Runnable() + { + // This part is run on swing thread + @Override + public void run () + { + // Update best values + bestIndex.setValue(index); + bestError.setValue(mse); + + if (errorListener != null) { + errorListener.stateChanged(errorEvent); + } + + // Remember starting time + startTime = System.currentTimeMillis(); + } + }); + } + + //--------// + // update // + //--------// + /** + * Specific behavior when a new task activity is notified. In addition to + * {@link TrainingPanel#update}, actions specific to training a neural + * network are handled here. + * + * @param obs the task object + * @param unused not used + */ + @Override + public void update (Observable obs, + Object unused) + { + super.update(obs, unused); + + switch (task.getActivity()) { + case INACTIVE: + incrementalTrainAction.setEnabled(true); + stopAction.setEnabled(false); + + break; + + case SELECTING: + incrementalTrainAction.setEnabled(false); + stopAction.setEnabled(false); + + break; + + case TRAINING: + incrementalTrainAction.setEnabled(false); + stopAction.setEnabled(true); + inputParams(); + displayParams(); + bestMse = Double.MAX_VALUE; + bestSnap = null; + + break; + } + + bestAction.setEnabled(false); + lastAction.setEnabled(false); + } + + //----------------------// + // defineSpecificLayout // + //----------------------// + private void defineSpecificLayout () + { + int r = 3; + // ETA field + builder.add(eta.getLabel(), cst.xy(9, r)); + builder.add(eta.getField(), cst.xyw(11, r, 5)); + + // Neural network parameters + r += 2; // ---------------------------- + builder.add(momentum.getLabel(), cst.xy(9, r)); + builder.add(momentum.getField(), cst.xy(11, r)); + + builder.add(learningRate.getLabel(), cst.xy(13, r)); + builder.add(learningRate.getField(), cst.xy(15, r)); + + r += 2; // ---------------------------- + builder.add(listEpochs.getLabel(), cst.xy(9, r)); + builder.add(listEpochs.getField(), cst.xy(11, r)); + + builder.add(maxError.getLabel(), cst.xy(13, r)); + builder.add(maxError.getField(), cst.xy(15, r)); + + // Training entities + r += 2; // ---------------------------- + + JButton dumpButton = new JButton(new DumpAction()); + dumpButton.setToolTipText("Dump the evaluator internals"); + + JButton trainButton = new JButton(trainAction); + trainButton.setToolTipText("Re-Train the evaluator from scratch"); + + JButton bestButton = new JButton(bestAction); + bestButton.setToolTipText("Use the weights of best snap"); + + builder.add(dumpButton, cst.xy(3, r)); + builder.add(trainButton, cst.xy(5, r)); + builder.add(bestButton, cst.xy(7, r)); + + builder.add(bestIndex.getLabel(), cst.xy(9, r)); + builder.add(bestIndex.getField(), cst.xy(11, r)); + + builder.add(bestError.getLabel(), cst.xy(13, r)); + builder.add(bestError.getField(), cst.xy(15, r)); + + r += 2; // ---------------------------- + + JButton stopButton = new JButton(stopAction); + stopButton.setToolTipText("Stop the training of the evaluator"); + + JButton incTrainButton = new JButton(incrementalTrainAction); + incTrainButton.setToolTipText("Incrementally train the evaluator"); + + JButton lastButton = new JButton(lastAction); + lastButton.setToolTipText("Use the last weights"); + + builder.add(stopButton, cst.xy(3, r)); + builder.add(incTrainButton, cst.xy(5, r)); + builder.add(lastButton, cst.xy(7, r)); + + builder.add(trainIndex.getLabel(), cst.xy(9, r)); + builder.add(trainIndex.getField(), cst.xy(11, r)); + + builder.add(trainError.getLabel(), cst.xy(13, r)); + builder.add(trainError.getField(), cst.xy(15, r)); + } + + //---------------// + // displayParams // + //---------------// + private void displayParams () + { + GlyphNetwork network = (GlyphNetwork) engine; + listEpochs.setValue(network.getListEpochs()); + learningRate.setValue(network.getLearningRate()); + momentum.setValue(network.getMomentum()); + maxError.setValue(network.getMaxError()); + } + + //-------------// + // inputParams // + //-------------// + private void inputParams () + { + GlyphNetwork network = (GlyphNetwork) engine; + network.setListEpochs(listEpochs.getValue()); + network.setLearningRate(learningRate.getValue()); + network.setMomentum(momentum.getValue()); + network.setMaxError(maxError.getValue()); + + progressBar.setMaximum(network.getListEpochs()); + } + + //~ Inner Classes ---------------------------------------------------------- + //------------// + // BestAction // + //------------// + private class BestAction + extends AbstractAction + { + //~ Constructors ------------------------------------------------------- + + public BestAction () + { + super("Use Best"); + } + + //~ Methods ------------------------------------------------------------ + @Override + public void actionPerformed (ActionEvent e) + { + GlyphNetwork glyphNetwork = (GlyphNetwork) engine; + NeuralNetwork network = glyphNetwork.getNetwork(); + network.restore(bestSnap); + logger.info("Network remaining error : {}", (float) bestMse); + glyphNetwork.marshal(); + + // Let the user choose the other possibility + setEnabled(false); + lastAction.setEnabled(true); + } + } + + //------------// + // LastAction // + //------------// + private class LastAction + extends AbstractAction + { + //~ Constructors ------------------------------------------------------- + + public LastAction () + { + super("Use Last"); + } + + //~ Methods ------------------------------------------------------------ + @Override + public void actionPerformed (ActionEvent e) + { + // Ask user confirmation if needed + if (lastMse > bestMse) { + final int answer = JOptionPane.showConfirmDialog( + component, + "Do you want to switch to this non-optimal network ?"); + + if (answer != JOptionPane.YES_OPTION) { + return; + } + } + + GlyphNetwork glyphNetwork = (GlyphNetwork) engine; + NeuralNetwork network = glyphNetwork.getNetwork(); + network.restore(lastSnap); + logger.info("Network remaining error : {}", (float) lastMse); + glyphNetwork.marshal(); + + // Let the user choose the other possibility + setEnabled(false); + bestAction.setEnabled(true); + } + } + + //--------------------// + // NetworkTrainAction // + //--------------------// + private class NetworkTrainAction + extends TrainingPanel.TrainAction + { + //~ Constructors ------------------------------------------------------- + + public NetworkTrainAction (String title, + EvaluationEngine.StartingMode mode, + boolean confirmationRequired) + { + super(title); + this.mode = mode; + this.confirmationRequired = confirmationRequired; + } + + //~ Methods ------------------------------------------------------------ + //-------// + // train // + //-------// + @Override + public void train () + { + super.train(); + + NeuralNetwork network = ((GlyphNetwork) engine).getNetwork(); + lastSnap = network.backup(); + + // By default, keep the better between best recorded and last + if (lastMse <= bestMse) { + lastAction.actionPerformed(null); + } else { + bestAction.actionPerformed(null); + } + } + } + + //-------------// + // ParamAction // + //-------------// + private class ParamAction + extends AbstractAction + { + //~ Methods ------------------------------------------------------------ + + // Purpose is just to read and remember the data from the various + // input fields. Triggered when user presses Enter in one of these + // fields. + @Override + public void actionPerformed (ActionEvent e) + { + inputParams(); + displayParams(); + } + } + + //------------// + // StopAction // + //------------// + private class StopAction + extends AbstractAction + { + //~ Constructors ------------------------------------------------------- + + public StopAction () + { + super("Stop"); + } + + //~ Methods ------------------------------------------------------------ + @Override + public void actionPerformed (ActionEvent e) + { + engine.stop(); + } + } +} diff --git a/src/main/omr/glyph/ui/panel/RegressionPanel.java b/src/main/omr/glyph/ui/panel/RegressionPanel.java new file mode 100644 index 0000000..199d342 --- /dev/null +++ b/src/main/omr/glyph/ui/panel/RegressionPanel.java @@ -0,0 +1,84 @@ +//----------------------------------------------------------------------------// +// // +// R e g r e s s i o n P a n e l // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.glyph.ui.panel; + +import omr.glyph.GlyphRegression; +import omr.glyph.ui.panel.TrainingPanel.DumpAction; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.swing.JButton; + +/** + * Class {@code RegressionPanel} is the user interface that handles the + * training of the linear engine. It is a dedicated companion of class + * {@link GlyphTrainer}. + * + * @author Hervé Bitteur + */ +class RegressionPanel + extends TrainingPanel +{ + //~ Static fields/initializers --------------------------------------------- + + /** Usual logger utility */ + private static final Logger logger = LoggerFactory.getLogger( + RegressionPanel.class); + + //~ Constructors ----------------------------------------------------------- + //-----------------// + // RegressionPanel // + //-----------------// + /** + * Creates a new RegressionPanel object. + * + * @param task the current training activity + * @param standardWidth standard width for fields & buttons + * @param selectionPanel the panel for glyph repository + */ + public RegressionPanel (GlyphTrainer.Task task, + String standardWidth, + SelectionPanel selectionPanel) + { + super( + task, + standardWidth, + GlyphRegression.getInstance(), + selectionPanel, + 4); + task.addObserver(this); + + trainAction = new TrainAction("Train"); + + defineSpecificLayout(); + } + + //~ Methods ---------------------------------------------------------------- + //----------------------// + // defineSpecificLayout // + //----------------------// + private void defineSpecificLayout () + { + int r = 7; + + // Training entities + JButton dumpButton = new JButton(new DumpAction()); + dumpButton.setToolTipText("Dump the evaluator internals"); + + JButton trainButton = new JButton(trainAction); + trainButton.setToolTipText("Train the evaluator from scratch"); + + builder.add(dumpButton, cst.xy(3, r)); + builder.add(trainButton, cst.xy(5, r)); + } +} diff --git a/src/main/omr/glyph/ui/panel/SelectionPanel.java b/src/main/omr/glyph/ui/panel/SelectionPanel.java new file mode 100644 index 0000000..22a24ac --- /dev/null +++ b/src/main/omr/glyph/ui/panel/SelectionPanel.java @@ -0,0 +1,603 @@ +//----------------------------------------------------------------------------// +// // +// S e l e c t i o n P a n e l // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.glyph.ui.panel; + +import omr.constant.Constant; +import omr.constant.ConstantSet; + +import omr.glyph.EvaluationEngine; +import omr.glyph.GlyphRegression; +import omr.glyph.GlyphRepository; +import omr.glyph.Shape; +import omr.glyph.facets.Glyph; +import static omr.glyph.ui.panel.GlyphTrainer.Task.Activity.*; + +import omr.ui.field.LIntegerField; +import omr.ui.util.Panel; + +import com.jgoodies.forms.builder.PanelBuilder; +import com.jgoodies.forms.layout.CellConstraints; +import com.jgoodies.forms.layout.FormLayout; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.awt.event.ActionEvent; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Observable; +import java.util.Observer; +import java.util.Set; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +import javax.swing.AbstractAction; +import javax.swing.Action; +import javax.swing.JButton; +import javax.swing.JComponent; +import javax.swing.JProgressBar; +import javax.swing.KeyStroke; + +/** + * Class {@code SelectionPanel} handles a user panel to select names + * from glyph repository, either the whole population or a core set of + * glyphs. + * This class is a dedicated companion of {@link GlyphTrainer}. + * + * @author Hervé Bitteur + */ +class SelectionPanel + implements GlyphRepository.Monitor, Observer +{ + //~ Static fields/initializers --------------------------------------------- + + /** Specific application parameters */ + private static final Constants constants = new Constants(); + + /** Usual logger utility */ + private static final Logger logger = LoggerFactory.getLogger(SelectionPanel.class); + + //~ Instance fields -------------------------------------------------------- + /** Reference of network panel companion (TBI) */ + private TrainingPanel trainingPanel; + + /** Swing component */ + private final Panel component; + + /** Current activity */ + private final GlyphTrainer.Task task; + + /** Underlying repository of known glyphs */ + private final GlyphRepository repository = GlyphRepository.getInstance(); + + /** For asynchronous execution of the glyph selection */ + private ExecutorService executor = Executors.newSingleThreadExecutor(); + + /** Visual progression of the selection */ + private JProgressBar progressBar = new JProgressBar(); + + /** To dump the current selection of glyphs used for training/validation */ + private DumpAction dumpAction = new DumpAction(); + + /** To refresh the application wrt to the training material on disk */ + private RefreshAction refreshAction = new RefreshAction(); + + /** To select a core out of whole base */ + private SelectAction selectAction = new SelectAction(); + + /** Counter on loaded glyphs */ + private int nbLoaded; + + /** Input/output on maximum number of glyphs with same shape */ + private LIntegerField similar = new LIntegerField( + "Max Similar", + "Max number of similar shapes"); + + /** Displayed counter on existing glyph files */ + private LIntegerField totalFiles = new LIntegerField( + false, + "Total", + "Total number of glyph files"); + + /** Displayed counter on loaded glyphs */ + private LIntegerField nbLoadedFiles = new LIntegerField( + false, + "Loaded", + "Number of glyph files loaded so far"); + + /** Displayed counter on selected glyphs */ + private LIntegerField nbSelectedFiles = new LIntegerField( + false, + "Selected", + "Number of selected glyph files to load"); + + //~ Constructors ----------------------------------------------------------- + //----------------// + // SelectionPanel // + //----------------// + /** + * Creates a new SelectionPanel object. + * + * @param task the common training task object + * @param standardWidth standard width to be used for fields & buttons + */ + public SelectionPanel (GlyphTrainer.Task task, + String standardWidth) + { + this.task = task; + task.addObserver(this); + + component = new Panel(); + component.setNoInsets(); + + component.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT) + .put(KeyStroke.getKeyStroke("ENTER"), "readParams"); + component.getActionMap() + .put("readParams", new ParamAction()); + + displayParams(); + + defineLayout(standardWidth); + } + + //~ Methods ---------------------------------------------------------------- + //---------// + // getBase // + //---------// + /** + * Retrieve the selected collection of glyph names + * + * @param whole indicate whether the whole population is to be selected, or + * just the core + * @return the collection of selected glyphs names + */ + public List getBase (boolean whole) + { + nbLoaded = 0; + progressBar.setValue(nbLoaded); + + if (whole) { + return repository.getWholeBase(this); + } else { + return repository.getCoreBase(this); + } + } + + //--------------// + // getComponent // + //--------------// + /** + * Give access to the encapsulated swinb component + * + * @return the user panel + */ + public JComponent getComponent () + { + return component; + } + + //-------------// + // loadedGlyph // + //-------------// + /** + * Call-back when a glyph has just been loaded + * + * @param gName the normalized glyph name + */ + @Override + public void loadedGlyph (String gName) + { + nbLoadedFiles.setValue(++nbLoaded); + progressBar.setValue(nbLoaded); + } + + //-------------------// + // setSelectedGlyphs // + //-------------------// + /** + * Notify the number of glyphs selected + * + * @param selected number of selected glyphs + */ + @Override + public void setSelectedGlyphs (int selected) + { + nbSelectedFiles.setValue(selected); + } + + //----------------// + // setTotalGlyphs // + //----------------// + /** + * Notify the total number of glyphs in the base + * + * @param total the total number of glyphs available + */ + @Override + public void setTotalGlyphs (int total) + { + totalFiles.setValue(total); + progressBar.setMaximum(total); + } + + //--------// + // update // + //--------// + /** + * Method triggered whenever the activity changes + * + * @param obs the new current task activity + * @param unused not used + */ + @Override + public void update (Observable obs, + Object unused) + { + switch (task.getActivity()) { + case INACTIVE: + selectAction.setEnabled(true); + + break; + + case SELECTING: + case TRAINING: + selectAction.setEnabled(false); + + break; + } + } + + //------------------// + // setTrainingPanel // + //------------------// + void setTrainingPanel (TrainingPanel trainingPanel) + { + this.trainingPanel = trainingPanel; + } + + //------------// + // defineCore // + //------------// + private void defineCore () + { + // What for ? TODO + inputParams(); + + // Train regression on them + GlyphRegression regression = GlyphRegression.getInstance(); + Collection gNames = getBase(true); // use whole + List glyphs = new ArrayList<>(); + + // Actually load each glyph description, if not yet done + for (String gName : gNames) { + Glyph glyph = repository.getGlyph(gName, this); + + if (glyph != null) { + glyphs.add(glyph); + } + } + + // Quickly train the regression evaluator (on the whole base) + regression.train(glyphs, null, EvaluationEngine.StartingMode.SCRATCH); + + // Measure all glyphs of each shape + Map> palmares = new HashMap<>(); + + for (String gName : gNames) { + Glyph glyph = repository.getGlyph(gName, this); + + if (glyph != null) { + try { + Shape shape = glyph.getShape(); + double grade = regression.measureDistance( + glyph, + shape); + List shapeNotes = palmares.get(shape); + + if (shapeNotes == null) { + shapeNotes = new ArrayList<>(); + palmares.put(shape, shapeNotes); + } + + shapeNotes.add(new NotedGlyph(gName, glyph, grade)); + } catch (Exception ex) { + logger.warn("Cannot evaluate {}", glyph); + } + } + } + + // Set of chosen shapes + final Set set = new HashSet<>(); + final int maxSimilar = similar.getValue(); + + // Sort the palmares, shape by shape, by (decreasing) grade + for (List shapeNotes : palmares.values()) { + Collections.sort(shapeNotes, NotedGlyph.reverseGradeComparator); + + // Take a sample equally distributed on instances of this shape + final int size = shapeNotes.size(); + final float delta = ((float) (size - 1)) / (maxSimilar - 1); + + for (int i = 0; i < maxSimilar; i++) { + int idx = Math.min(size - 1, Math.round(i * delta)); + NotedGlyph ng = shapeNotes.get(idx); + + if (ng.glyph.getShape() + .isTrainable()) { + set.add(ng); + } + } + } + + // Build the core base + List base = new ArrayList<>(set.size()); + + for (NotedGlyph ng : set) { + base.add(ng.gName); + } + + repository.setCoreBase(base); + setSelectedGlyphs(base.size()); + } + + //--------------// + // defineLayout // + //--------------// + private void defineLayout (String standardWidth) + { + FormLayout layout = Panel.makeFormLayout( + 3, + 4, + "", + standardWidth, + standardWidth); + PanelBuilder builder = new PanelBuilder(layout, component); + CellConstraints cst = new CellConstraints(); + builder.setDefaultDialogBorder(); + + int r = 1; // ---------------------------- + builder.addSeparator("Selection", cst.xyw(1, r, 7)); + builder.add(progressBar, cst.xyw(9, r, 7)); + + r += 2; // ---------------------------- + builder.add(new JButton(dumpAction), cst.xy(3, r)); + builder.add(new JButton(refreshAction), cst.xy(5, r)); + + builder.add(similar.getLabel(), cst.xy(9, r)); + builder.add(similar.getField(), cst.xy(11, r)); + + builder.add(totalFiles.getLabel(), cst.xy(13, r)); + builder.add(totalFiles.getField(), cst.xy(15, r)); + + r += 2; // ---------------------------- + builder.add(new JButton(selectAction), cst.xy(3, r)); + builder.add(nbSelectedFiles.getLabel(), cst.xy(9, r)); + builder.add(nbSelectedFiles.getField(), cst.xy(11, r)); + + builder.add(nbLoadedFiles.getLabel(), cst.xy(13, r)); + builder.add(nbLoadedFiles.getField(), cst.xy(15, r)); + } + + //---------------// + // displayParams // + //---------------// + private void displayParams () + { + similar.setValue(constants.maxSimilar.getValue()); + } + + //-------------// + // inputParams // + //-------------// + private void inputParams () + { + constants.maxSimilar.setValue(similar.getValue()); + } + + //~ Inner Classes ---------------------------------------------------------- + //-----------// + // Constants // + //-----------// + private static final class Constants + extends ConstantSet + { + //~ Instance fields ---------------------------------------------------- + + Constant.Integer maxSimilar = new Constant.Integer( + "Glyphs", + 10, + "Absolute maximum number of instances for the same shape" + + " used in training"); + + } + + //------------// + // NotedGlyph // + //------------// + /** + * Handle a glyph together with its name and grade + */ + private static class NotedGlyph + { + //~ Static fields/initializers ----------------------------------------- + + /** For comparing NotedGlyph instance in reverse grade order */ + static final Comparator reverseGradeComparator = new Comparator() + { + @Override + public int compare (NotedGlyph ng1, + NotedGlyph ng2) + { + return Double.compare(ng2.grade, ng1.grade); + } + }; + + //~ Instance fields ---------------------------------------------------- + final String gName; + + final Glyph glyph; + + final double grade; + + //~ Constructors ------------------------------------------------------- + public NotedGlyph (String gName, + Glyph glyph, + double grade) + { + this.gName = gName; + this.glyph = glyph; + this.grade = grade; + } + + //~ Methods ------------------------------------------------------------ + @Override + public String toString () + { + return "{NotedGlyph " + gName + " " + grade + "}"; + } + } + + //------------// + // DumpAction // + //------------// + private class DumpAction + extends AbstractAction + { + //~ Constructors ------------------------------------------------------- + + public DumpAction () + { + super("Dump"); + putValue( + Action.SHORT_DESCRIPTION, + "Dump the current glyph selection"); + } + + //~ Methods ------------------------------------------------------------ + @Override + public void actionPerformed (ActionEvent e) + { + List gNames = getBase(trainingPanel.useWhole()); + System.out.println( + "Content of " + + (trainingPanel.useWhole() ? "whole" : "core") + + " population (" + gNames.size() + "):"); + Collections.sort(gNames, GlyphRepository.shapeComparator); + + int glyphNb = 0; + String prevName = null; + + for (String gName : gNames) { + if (prevName != null) { + if (!GlyphRepository.shapeNameOf(gName) + .equals(prevName)) { + System.out.println( + String.format("%4d %s", glyphNb, prevName)); + glyphNb = 1; + } + } + + glyphNb++; + prevName = GlyphRepository.shapeNameOf(gName); + } + + System.out.println(String.format("%4d %s", glyphNb, prevName)); + } + } + + //-------------// + // ParamAction // + //-------------// + private class ParamAction + extends AbstractAction + { + //~ Methods ------------------------------------------------------------ + + // Purpose is just to read and remember the data from the various + // input fields. Triggered when user presses Enter in one of these + // fields. + @Override + public void actionPerformed (ActionEvent e) + { + inputParams(); + displayParams(); + } + } + + //---------------// + // RefreshAction // + //---------------// + private class RefreshAction + extends AbstractAction + { + //~ Constructors ------------------------------------------------------- + + public RefreshAction () + { + super("Disk Refresh"); + putValue( + Action.SHORT_DESCRIPTION, + "Refresh trainer with disk information"); + } + + //~ Methods ------------------------------------------------------------ + @Override + public void actionPerformed (ActionEvent e) + { + repository.refreshBases(); + } + } + + //--------------// + // SelectAction // + //--------------// + private class SelectAction + extends AbstractAction + { + //~ Constructors ------------------------------------------------------- + + public SelectAction () + { + super("Select Core"); + putValue( + Action.SHORT_DESCRIPTION, + "Build core selection out of whole glyph base"); + } + + //~ Methods ------------------------------------------------------------ + @Override + public void actionPerformed (ActionEvent e) + { + executor.execute( + new Runnable() + { + @Override + public void run () + { + task.setActivity(SELECTING); + + // Define Core from Whole + defineCore(); + repository.storeCoreBase(); + + task.setActivity(INACTIVE); + } + }); + } + } +} diff --git a/src/main/omr/glyph/ui/panel/TrainingPanel.java b/src/main/omr/glyph/ui/panel/TrainingPanel.java new file mode 100644 index 0000000..322c66d --- /dev/null +++ b/src/main/omr/glyph/ui/panel/TrainingPanel.java @@ -0,0 +1,508 @@ +//----------------------------------------------------------------------------// +// // +// T r a i n i n g P a n e l // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.glyph.ui.panel; + +import omr.glyph.*; +import omr.glyph.GlyphNetwork; +import omr.glyph.GlyphRepository; +import omr.glyph.Shape; +import static omr.glyph.Shape.*; +import omr.glyph.facets.Glyph; +import static omr.glyph.ui.panel.GlyphTrainer.Task.Activity.*; + +import omr.ui.util.Panel; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.jgoodies.forms.builder.PanelBuilder; +import com.jgoodies.forms.layout.CellConstraints; +import com.jgoodies.forms.layout.FormLayout; + +import java.awt.event.ActionEvent; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Iterator; +import java.util.List; +import java.util.Observable; +import java.util.Observer; + +import javax.swing.AbstractAction; +import javax.swing.ButtonGroup; +import javax.swing.JComponent; +import javax.swing.JLabel; +import javax.swing.JOptionPane; +import javax.swing.JProgressBar; +import javax.swing.JRadioButton; +import javax.swing.SwingWorker; + +/** + * Class {@code TrainingPanel} is a panel dedicated to the training of + * an evaluation engine. + * It is used through its subclasses {@link NetworkPanel} and {@link + * RegressionPanel} to train the neural network engine and the linear + * engine respectively. It is a dedicated companion of class {@link + * GlyphTrainer}. + * + * @author Hervé Bitteur + */ +class TrainingPanel + implements EvaluationEngine.Monitor, Observer +{ + //~ Static fields/initializers --------------------------------------------- + + /** Usual logger utility */ + private static final Logger logger = LoggerFactory.getLogger(TrainingPanel.class); + + //~ Instance fields -------------------------------------------------------- + + /** The swing component */ + protected final Panel component; + + /** Current activity (selecting the population, or training the engine on + * the selected population */ + protected final GlyphTrainer.Task task; + + /** User action to launch the training */ + protected TrainAction trainAction; + + /** The underlying engine to be trained */ + protected EvaluationEngine engine; + + /** User progress bar to visualize the training process */ + protected JProgressBar progressBar = new JProgressBar(); + + /** Common JGoodies constraints for this class and its subclass if any */ + protected CellConstraints cst = new CellConstraints(); + + /** Common JGoodies builder for this class and its subclass if any */ + protected PanelBuilder builder; + + /** Repository of known glyphs */ + private final GlyphRepository repository = GlyphRepository.getInstance(); + + /** + * Flag to indicate that the whole population of recorded glyphs (and not + * just the core ones) is to be considered + */ + private boolean useWhole = true; + + /** Display of cardinality of whole population */ + private JLabel wholeNumber = new JLabel(); + + /** Display of cardinality of core population */ + private JLabel coreNumber = new JLabel(); + + /** UI panel dealing with repository selection */ + private final SelectionPanel selectionPanel; + + /** The Neural Network engine */ + private GlyphNetwork network = GlyphNetwork.getInstance(); + + //~ Constructors ----------------------------------------------------------- + + //---------------// + // TrainingPanel // + //---------------// + /** + * Creates a new TrainingPanel object. + * + * @param task the current training task + * @param standardWidth standard width for fields & buttons + * @param engine the underlying engine to train + * @param selectionPanel user panel for glyphs selection + * @param totalRows total number of display rows, interlines not + * counted + */ + public TrainingPanel (GlyphTrainer.Task task, + String standardWidth, + EvaluationEngine engine, + SelectionPanel selectionPanel, + int totalRows) + { + this.engine = engine; + this.task = task; + this.selectionPanel = selectionPanel; + + component = new Panel(); + component.setNoInsets(); + + FormLayout layout = Panel.makeFormLayout( + totalRows, + 4, + "", + standardWidth, + standardWidth); + + builder = new PanelBuilder(layout, component); + builder.setDefaultDialogBorder(); // Useful ? + + defineLayout(); + } + + //~ Methods ---------------------------------------------------------------- + + @Override + public void epochEnded (int epochIndex, + double mse) + { + } + + //--------------// + // getComponent // + //--------------// + /** + * Give access to the encapsulated swing component + * + * @return the user panel + */ + public JComponent getComponent () + { + return component; + } + + @Override + public void glyphProcessed (final Glyph glyph) + { + } + + @Override + public void trainingStarted (final int epochIndex, + final double mse) + { + } + + //--------// + // update // + //--------// + /** + * Method triggered by new task activity : the train action is enabled only + * when no activity is going on. + * + * @param obs the task object + * @param unused not used + */ + @Override + public void update (Observable obs, + Object unused) + { + switch (task.getActivity()) { + case INACTIVE : + trainAction.setEnabled(true); + + break; + + case SELECTING : + case TRAINING : + trainAction.setEnabled(false); + + break; + } + } + + //----------// + // useWhole // + //----------// + /** + * Tell whether the whole glyph base is to be used, or just the core base + * + * @return true if whole, false if core + */ + public boolean useWhole () + { + return useWhole; + } + + //--------------// + // defineLayout // + //--------------// + /** + * Define the common part of the layout, each subclass being able to augment + * this layout from its constructor + */ + protected void defineLayout () + { + // Buttons to select just the core glyphs, or the whole population + CoreAction coreAction = new CoreAction(); + JRadioButton coreButton = new JRadioButton(coreAction); + WholeAction wholeAction = new WholeAction(); + JRadioButton wholeButton = new JRadioButton(wholeAction); + + // Group the radio buttons. + ButtonGroup group = new ButtonGroup(); + group.add(wholeButton); + wholeButton.setToolTipText("Use the whole glyph base for any action"); + group.add(coreButton); + coreButton.setToolTipText( + "Use only the core glyph base for any action"); + wholeButton.setSelected(true); + + // Evaluator Title & Progress Bar + int r = 1; // ---------------------------- + String title = engine.getName() + " Training"; + builder.addSeparator(title, cst.xyw(1, r, 7)); + builder.add(progressBar, cst.xyw(9, r, 7)); + + r += 2; // ---------------------------- + builder.add(wholeButton, cst.xy(3, r)); + builder.add(wholeNumber, cst.xy(5, r)); + + r += 2; // ---------------------------- + builder.add(coreButton, cst.xy(3, r)); + builder.add(coreNumber, cst.xy(5, r)); + + // Initialize with population cardinalities + coreAction.actionPerformed(null); + wholeAction.actionPerformed(null); + } + + //-----------------// + // checkPopulation // + //-----------------// + private void checkPopulation (List glyphs) + { + // Check that all trainable shapes are present in the training + // population and that only legal shapes are present. If illegal + // (non trainable) shapes are found, they are removed from the + // population. + boolean[] present = new boolean[LAST_PHYSICAL_SHAPE.ordinal() + 1]; + Arrays.fill(present, false); + + for (Iterator it = glyphs.iterator(); it.hasNext();) { + Glyph glyph = it.next(); + Shape shape = glyph.getShape(); + + try { + Shape physicalShape = shape.getPhysicalShape(); + + if (physicalShape.isTrainable()) { + present[physicalShape.ordinal()] = true; + } else { + logger.warn("Removing non trainable shape:{}", physicalShape); + it.remove(); + } + } catch (Exception ex) { + logger.warn("Removing weird shape: " + shape, ex); + it.remove(); + } + } + + for (int i = 0; i < present.length; i++) { + if (!present[i]) { + logger.warn("Missing shape: {}", Shape.values()[i]); + } + } + } + + //~ Inner Classes ---------------------------------------------------------- + + //------------// + // DumpAction // + //------------// + protected class DumpAction + extends AbstractAction + { + //~ Constructors ------------------------------------------------------- + + public DumpAction () + { + super("Dump"); + } + + //~ Methods ------------------------------------------------------------ + + @Override + public void actionPerformed (ActionEvent e) + { + engine.dump(); + } + } + + //-------------// + // TrainAction // + //-------------// + protected class TrainAction + extends AbstractAction + { + //~ Instance fields ---------------------------------------------------- + + // Specific training starting mode + protected EvaluationEngine.StartingMode mode = EvaluationEngine.StartingMode.SCRATCH; + protected boolean confirmationRequired = true; + + //~ Constructors ------------------------------------------------------- + + public TrainAction (String title) + { + super(title); + } + + //~ Methods ------------------------------------------------------------ + + @Override + public void actionPerformed (ActionEvent e) + { + // Ask user confirmation + if (confirmationRequired) { + int answer = JOptionPane.showConfirmDialog( + component, + "Do you really want to retrain " + engine.getName() + + " from scratch?"); + + if (answer != JOptionPane.YES_OPTION) { + return; + } + } + + class Worker + extends Thread + { + @Override + public void run () + { + train(); + } + } + + Worker worker = new Worker(); + worker.setPriority(Thread.MIN_PRIORITY); + worker.start(); + } + + //-------// + // train // + //-------// + public void train () + { + task.setActivity(TRAINING); + + Collection gNames = selectionPanel.getBase(useWhole); + progressBar.setValue(0); + progressBar.setMaximum(network.getListEpochs()); + + List glyphs = new ArrayList<>(); + + for (String gName : gNames) { + Glyph glyph = repository.getGlyph(gName, selectionPanel); + + if (glyph != null) { + if (glyph.getShape() != null) { + glyphs.add(glyph); + } else { + logger.warn("Cannot infer shape from {}", gName); + } + } else { + logger.warn("Cannot get glyph {}", gName); + } + } + + // Check that all trainable shapes (and only those ones) are + // present in the training population + checkPopulation(glyphs); + + engine.train(glyphs, TrainingPanel.this, mode); + + task.setActivity(INACTIVE); + } + } + + //------------// + // CoreAction // + //------------// + private class CoreAction + extends AbstractAction + { + //~ Instance fields ---------------------------------------------------- + + final SwingWorker worker = new SwingWorker() { + @Override + public void done () + { + try { + coreNumber.setText("" + get()); + } catch (Exception ex) { + logger.warn("Error while loading core base", ex); + } + } + + @Override + protected Integer doInBackground () + { + return selectionPanel.getBase(false) + .size(); + } + }; + + + //~ Constructors ------------------------------------------------------- + + public CoreAction () + { + super("Core"); + } + + //~ Methods ------------------------------------------------------------ + + @Override + public void actionPerformed (ActionEvent e) + { + useWhole = false; + worker.execute(); + } + } + + //-------------// + // WholeAction // + //-------------// + private class WholeAction + extends AbstractAction + { + //~ Instance fields ---------------------------------------------------- + + final SwingWorker worker = new SwingWorker() { + @Override + public void done () + { + try { + wholeNumber.setText("" + get()); + } catch (Exception ex) { + logger.warn("Error while loading whole base", ex); + } + } + + @Override + protected Integer doInBackground () + { + return selectionPanel.getBase(true) + .size(); + } + }; + + + //~ Constructors ------------------------------------------------------- + + public WholeAction () + { + super("Whole"); + } + + //~ Methods ------------------------------------------------------------ + + @Override + public void actionPerformed (ActionEvent e) + { + useWhole = true; + worker.execute(); + } + } +} diff --git a/src/main/omr/glyph/ui/panel/ValidationPanel.java b/src/main/omr/glyph/ui/panel/ValidationPanel.java new file mode 100644 index 0000000..608552d --- /dev/null +++ b/src/main/omr/glyph/ui/panel/ValidationPanel.java @@ -0,0 +1,389 @@ +//----------------------------------------------------------------------------// +// // +// V a l i d a t i o n P a n e l // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.glyph.ui.panel; + +import omr.glyph.Evaluation; +import omr.glyph.ShapeEvaluator; +import omr.glyph.GlyphRepository; +import omr.glyph.Grades; +import omr.glyph.facets.Glyph; +import omr.glyph.ui.SampleVerifier; + +import omr.ui.field.LDoubleField; +import omr.ui.field.LIntegerField; +import omr.ui.util.Panel; + +import com.jgoodies.forms.builder.PanelBuilder; +import com.jgoodies.forms.layout.CellConstraints; +import com.jgoodies.forms.layout.FormLayout; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.awt.event.ActionEvent; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Observable; +import java.util.Observer; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +import javax.swing.AbstractAction; +import javax.swing.JButton; +import javax.swing.JComponent; +import javax.swing.JProgressBar; + +/** + * Class {@code ValidationPanel} handles the validation of an evaluator + * against the selected population of glyphs (either the whole base or + * the core base). + * It is a dedicated companion of class {@link GlyphTrainer}. + * + * @author Hervé Bitteur + */ +class ValidationPanel + implements Observer +{ + //~ Static fields/initializers --------------------------------------------- + + /** Usual logger utility */ + private static final Logger logger = LoggerFactory.getLogger( + ValidationPanel.class); + + //~ Instance fields -------------------------------------------------------- + /** Swing component */ + private final Panel component; + + /** Dedicated executor for validation */ + private ExecutorService executor = Executors.newSingleThreadExecutor(); + + /** The evaluator to validate */ + private final ShapeEvaluator evaluator; + + /** User progress bar to visualize the validation process */ + private JProgressBar progressBar = new JProgressBar(); + + /** Repository of known glyphs */ + private final GlyphRepository repository = GlyphRepository.getInstance(); + + /** User interface that handles glyphs selection */ + private final SelectionPanel selectionPanel; + + /** User interface that handles evaluator training */ + private final TrainingPanel trainingPanel; + + /** User action to validate the evaluator against whole or core base */ + private ValidateAction validateAction = new ValidateAction(); + + /** Display percentage of glyphs correctly recognized */ + private LDoubleField pcValue = new LDoubleField( + false, + "% OK", + "Percentage of recognized glyphs", + " %5.2f%%"); + + /** Display number of glyphs correctly recognized */ + private LIntegerField positiveValue = new LIntegerField( + false, + "Glyphs OK", + "Number of glyphs correctly recognized"); + + /** Display number of glyphs mistaken with some other shape */ + private LIntegerField falsePositiveValue = new LIntegerField( + false, + "False Pos.", + "Number of glyphs incorrectly recognized"); + + /** Collection of glyph names leading to false positives */ + private List falsePositives = new ArrayList<>(); + + /** User action to investigate on false positives */ + private FalsePositiveAction falsePositiveAction = new FalsePositiveAction(); + + /** Display number of glyphs not recognized */ + private LIntegerField negativeValue = new LIntegerField( + false, + "Negative", + "Number of glyphs not recognized"); + + /** Collection of glyph names not recognized (negatives) */ + private List negatives = new ArrayList<>(); + + /** User action to investigate on negatives */ + private NegativeAction negativeAction = new NegativeAction(); + + //~ Constructors ----------------------------------------------------------- + /** + * Creates a new ValidationPanel object. + * + * @param task the current training activity + * @param standardWidth standard width for fields & buttons + * @param evaluator the evaluator to validate + * @param selectionPanel user panel for glyph selection + * @param trainingPanel user panel for evaluator training + */ + public ValidationPanel (GlyphTrainer.Task task, + String standardWidth, + ShapeEvaluator evaluator, + SelectionPanel selectionPanel, + TrainingPanel trainingPanel) + { + this.evaluator = evaluator; + this.selectionPanel = selectionPanel; + this.trainingPanel = trainingPanel; + task.addObserver(this); + + component = new Panel(); + component.setNoInsets(); + + defineLayout(standardWidth); + } + + //~ Methods ---------------------------------------------------------------- + //--------------// + // getComponent // + //--------------// + /** + * Give access to the encapsulated swing component. + * + * @return the user panel + */ + public JComponent getComponent () + { + return component; + } + + //--------// + // update // + //--------// + /** + * A degenerated version, just to disable by default the + * verification actions whenever a new task activity is notified. + * These actions are then re-enabled only at the end of the validation run. + * + * @param obs not used + * @param unused not used + */ + @Override + public void update (Observable obs, + Object unused) + { + negativeAction.setEnabled(!negatives.isEmpty()); + falsePositiveAction.setEnabled(!falsePositives.isEmpty()); + } + + //--------------// + // defineLayout // + //--------------// + private void defineLayout (String standardWidth) + { + /** Common JGoogies constraints for this class and its subclass if any */ + CellConstraints cst = new CellConstraints(); + + /** Common JGoogies builder for this class and its subclass if any */ + FormLayout layout = Panel.makeFormLayout( + 3, + 4, + "", + standardWidth, + standardWidth); + PanelBuilder builder = new PanelBuilder(layout, component); + + // Validation title & progress bar + int r = 1; + builder.addSeparator("Validation", cst.xyw(1, r, 7)); + builder.add(progressBar, cst.xyw(9, r, 7)); + + r += 2; // ---------------------------- + + builder.add(positiveValue.getLabel(), cst.xy(5, r)); + builder.add(positiveValue.getField(), cst.xy(7, r)); + builder.add(negativeValue.getLabel(), cst.xy(9, r)); + builder.add(negativeValue.getField(), cst.xy(11, r)); + builder.add(falsePositiveValue.getLabel(), cst.xy(13, r)); + builder.add(falsePositiveValue.getField(), cst.xy(15, r)); + + r += 2; // ---------------------------- + + JButton validateButton = new JButton(validateAction); + validateButton.setToolTipText( + "Validate the evaluator on current base of glyphs"); + + JButton negativeButton = new JButton(negativeAction); + negativeButton.setToolTipText( + "Display the impacted glyphs for verification"); + + JButton falsePositiveButton = new JButton(falsePositiveAction); + falsePositiveButton.setToolTipText( + "Display the impacted glyphs for verification"); + + builder.add(validateButton, cst.xy(3, r)); + builder.add(pcValue.getLabel(), cst.xy(5, r)); + builder.add(pcValue.getField(), cst.xy(7, r)); + builder.add(negativeButton, cst.xy(11, r)); + builder.add(falsePositiveButton, cst.xy(15, r)); + } + + //---------------// + // runValidation // + //---------------// + private void runValidation () + { + logger.info("Validating {} evaluator on {} base ...", + evaluator.getName(), + trainingPanel.useWhole() ? "whole" : "core"); + + // Empty the display + positiveValue.setText(""); + pcValue.setText(""); + negativeValue.setText(""); + falsePositiveValue.setText(""); + negativeAction.setEnabled(false); + falsePositiveAction.setEnabled(false); + + negatives.clear(); + falsePositives.clear(); + + int positives = 0; + Collection gNames = selectionPanel.getBase( + trainingPanel.useWhole()); + + progressBar.setValue(0); + progressBar.setMaximum(gNames.size()); + + int index = 0; + + for (String gName : gNames) { + index++; + + Glyph glyph = repository.getGlyph(gName, selectionPanel); + + if (glyph != null) { + Evaluation vote = evaluator.rawVote( + glyph, + Grades.validationMinGrade, + null); + + if (vote == null) { + negatives.add(gName); + System.out.printf("%-35s not recognized%n", gName); + } else if (vote.shape.getPhysicalShape() == glyph.getShape() + .getPhysicalShape()) { + positives++; + } else { + falsePositives.add(gName); + System.out.printf( + "%-35s mistaken for %s%n", + gName, + vote.shape.getPhysicalShape()); + } + } + + // Update progress bar + progressBar.setValue(index); + } + + int total = gNames.size(); + double pc = ((double) positives * 100) / (double) total; + String pcStr = String.format(" %5.2f%%", pc); + logger.info("{}Evaluator. Ratio={} : {}/{}", + evaluator.getName(), pcStr, positives, total); + positiveValue.setValue(positives); + pcValue.setValue(pc); + negativeValue.setValue(negatives.size()); + falsePositiveValue.setValue(falsePositives.size()); + } + + //~ Inner Classes ---------------------------------------------------------- + //---------------------// + // FalsePositiveAction // + //---------------------// + private class FalsePositiveAction + extends AbstractAction + { + //~ Constructors ------------------------------------------------------- + + public FalsePositiveAction () + { + super("Verify"); + } + + //~ Methods ------------------------------------------------------------ + @Override + public void actionPerformed (ActionEvent e) + { + SampleVerifier.getInstance() + .verify(falsePositives); + SampleVerifier.getInstance() + .setVisible(true); + } + } + + //----------------// + // NegativeAction // + //----------------// + private class NegativeAction + extends AbstractAction + { + //~ Constructors ------------------------------------------------------- + + public NegativeAction () + { + super("Verify"); + } + + //~ Methods ------------------------------------------------------------ + @Override + public void actionPerformed (ActionEvent e) + { + SampleVerifier.getInstance() + .verify(negatives); + SampleVerifier.getInstance() + .setVisible(true); + } + } + + //----------------// + // ValidateAction // + //----------------// + private class ValidateAction + extends AbstractAction + { + //~ Constructors ------------------------------------------------------- + + public ValidateAction () + { + super("Validate"); + } + + //~ Methods ------------------------------------------------------------ + @Override + public void actionPerformed (ActionEvent e) + { + executor.execute( + new Runnable() + { + @Override + public void run () + { + setEnabled(false); + runValidation(); + negativeAction.setEnabled(negatives.size() > 0); + falsePositiveAction.setEnabled( + !falsePositives.isEmpty()); + setEnabled(true); + } + }); + } + } +} diff --git a/src/main/omr/glyph/ui/panel/package.html b/src/main/omr/glyph/ui/panel/package.html new file mode 100644 index 0000000..4d9a649 --- /dev/null +++ b/src/main/omr/glyph/ui/panel/package.html @@ -0,0 +1,16 @@ + + + + + + Package omr.ui.panel + + + +

+ Package dedicated to UI glyph panels +

+ + + diff --git a/src/main/omr/glyph/ui/panel/resources/GlyphTrainer.properties b/src/main/omr/glyph/ui/panel/resources/GlyphTrainer.properties new file mode 100644 index 0000000..1f8c745 --- /dev/null +++ b/src/main/omr/glyph/ui/panel/resources/GlyphTrainer.properties @@ -0,0 +1,11 @@ +# ---------------------------------------------------------------------------- # +# # +# G l y p h T r a i n e r . p r o p e r t i e s # +# # +# ---------------------------------------------------------------------------- # + +# This file gathers resources to be injected in the GlyphTrainer class +# +# This is the generic version + +trainerFrame.title = ${Application.name} Trainer diff --git a/src/main/omr/glyph/ui/panel/resources/GlyphTrainer_fr_FR.properties b/src/main/omr/glyph/ui/panel/resources/GlyphTrainer_fr_FR.properties new file mode 100644 index 0000000..d8fc10c --- /dev/null +++ b/src/main/omr/glyph/ui/panel/resources/GlyphTrainer_fr_FR.properties @@ -0,0 +1,11 @@ +# ---------------------------------------------------------------------------- # +# # +# G l y p h T r a i n e r _ f r . p r o p e r t i e s # +# # +# ---------------------------------------------------------------------------- # + +# This file gathers resources to be injected in the GlyphTrainer class +# +# This is the "fr" version + +trainerFrame.title = ${Application.id} Apprentissage diff --git a/src/main/omr/glyph/ui/resources/SampleVerifier.properties b/src/main/omr/glyph/ui/resources/SampleVerifier.properties new file mode 100644 index 0000000..d3a5ea6 --- /dev/null +++ b/src/main/omr/glyph/ui/resources/SampleVerifier.properties @@ -0,0 +1,11 @@ +# ---------------------------------------------------------------------------- # +# # +# S a m p l e V e r i f i e r . p r o p e r t i e s # +# # +# ---------------------------------------------------------------------------- # + +# This file gathers resources to be injected in the SampleVerifier class +# +# This is the generic version + +SampleVerifierFrame.title = Sample Verifier diff --git a/src/main/omr/glyph/ui/resources/SampleVerifier_fr.properties b/src/main/omr/glyph/ui/resources/SampleVerifier_fr.properties new file mode 100644 index 0000000..d4c2a6a --- /dev/null +++ b/src/main/omr/glyph/ui/resources/SampleVerifier_fr.properties @@ -0,0 +1,11 @@ +# ---------------------------------------------------------------------------- # +# # +# S a m p l e V e r i f i e r _ f r . p r o p e r t i e s # +# # +# ---------------------------------------------------------------------------- # + +# This file gathers resources to be injected in the GlyphVerifierclass +# +# This is the "fr" version + +SampleVerifierFrame.title = V\u00e9rificateur des \u00e9chantillons \ No newline at end of file diff --git a/src/main/omr/glyph/ui/resources/ShapeColorChooser.properties b/src/main/omr/glyph/ui/resources/ShapeColorChooser.properties new file mode 100644 index 0000000..aa2a6de --- /dev/null +++ b/src/main/omr/glyph/ui/resources/ShapeColorChooser.properties @@ -0,0 +1,11 @@ +# ---------------------------------------------------------------------------- # +# # +# S h a p e C o l o r C h o o s e r . p r o p e r t i e s # +# # +# ---------------------------------------------------------------------------- # + +# This file gathers resources to be injected in the ShapeColorChooser class +# +# This is the generic version + +shapeColorChooserFrame.title = Shape Color Chooser \ No newline at end of file diff --git a/src/main/omr/glyph/ui/resources/ShapeColorChooser_fr.properties b/src/main/omr/glyph/ui/resources/ShapeColorChooser_fr.properties new file mode 100644 index 0000000..bcafd62 --- /dev/null +++ b/src/main/omr/glyph/ui/resources/ShapeColorChooser_fr.properties @@ -0,0 +1,11 @@ +# ---------------------------------------------------------------------------- # +# # +# S h a p e C o l o r C h o o s e r _ f r . p r o p e r t i e s # +# # +# ---------------------------------------------------------------------------- # + +# This file gathers resources to be injected in the ShapeColorChooser class +# +# This is the "fr" version + +shapeColorChooserFrame.title = Choix des Couleurs des Formes \ No newline at end of file diff --git a/src/main/omr/glyph/ui/resources/ViewParameters.properties b/src/main/omr/glyph/ui/resources/ViewParameters.properties new file mode 100644 index 0000000..d36477b --- /dev/null +++ b/src/main/omr/glyph/ui/resources/ViewParameters.properties @@ -0,0 +1,28 @@ +# ---------------------------------------------------------------------------- # +# # +# V i e w P a r a m e t e r s . p r o p e r t i e s # +# # +# ---------------------------------------------------------------------------- # + +# This file gathers resources to be injected in the ViewParameters class +# +# This is the generic version + +toggleLines.Action.text = Show stick Lines +toggleLines.Action.shortDescription = Show the line of every selected stick + +toggleLetters.Action.text = Show letter Boxes +toggleLetters.Action.shortDescription = Show the letters bounding boxes in every selected glyph + +toggleAttachments.Action.text = Show Attachments +toggleAttachments.Action.shortDescription = Show attachments + +toggleTranslations.Action.text = Show glyph Translations +toggleTranslations.Action.shortDescription = Show links to glyph translations + +toggleSentences.Action.text = Show sentences +toggleSentences.Action.shortDescription = Show links between words in a sentence + +toggleSections.Action.text = Enable Section selection +toggleSections.Action.shortDescription = Enable to select Sections rather than Glyphs +toggleSections.Action.icon = ${icons.root}/apps/kjumpingcube.png diff --git a/src/main/omr/glyph/ui/resources/ViewParameters_fr.properties b/src/main/omr/glyph/ui/resources/ViewParameters_fr.properties new file mode 100644 index 0000000..d161a96 --- /dev/null +++ b/src/main/omr/glyph/ui/resources/ViewParameters_fr.properties @@ -0,0 +1,27 @@ +# ---------------------------------------------------------------------------- # +# # +# V i e w P a r a m e t e r s _ f r . p r o p e r t i e s # +# # +# ---------------------------------------------------------------------------- # + +# This file gathers resources to be injected in the ViewParameters class +# +# This is the "fr" version + +toggleLines.Action.text = Afficher les axes moyens +toggleLines.Action.shortDescription = Afficher l'axe moyen de chaque \u00e9l\u00e9ment graphique s\u00e9lectionn\u00e9 + +toggleLetters.Action.text = Afficher les limites de lettres +toggleLetters.Action.shortDescription = Afficher les limites de lettre dans chaque \u00e9l\u00e9ment graphique s\u00e9lectionn\u00e9 + +toggleAttachments.Action.text = Afficher les attachements +toggleAttachments.Action.shortDescription = Afficher les attachements + +toggleTranslations.Action.text = Afficher les transcriptions +toggleTranslations.Action.shortDescription = Afficher les liens vers les transcriptions de glyphes + +toggleSentences.Action.text = Afficher les phrases +toggleSentences.Action.shortDescription = Afficher les liens entre les mots d'une m\u00eame phrase + +toggleSections.Action.text = Mode s\u00e9lection de Sections +toggleSections.Action.shortDescription = Permettre la s\u00e9lection de sections plut\u00f4t que de glyphes diff --git a/src/main/omr/graph/BasicDigraph.java b/src/main/omr/graph/BasicDigraph.java new file mode 100644 index 0000000..97c31d8 --- /dev/null +++ b/src/main/omr/graph/BasicDigraph.java @@ -0,0 +1,243 @@ +//----------------------------------------------------------------------------// +// // +// B a s i c D i g r a p h // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.graph; + +import net.jcip.annotations.ThreadSafe; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Collection; +import java.util.Collections; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * Class {@code BasicDigraph} is a basic implementation of Digraph. + * + * @author Hervé Bitteur + */ +@ThreadSafe +public class BasicDigraph, V extends Vertex> + implements Digraph +{ + //~ Static fields/initializers --------------------------------------------- + + /** Usual logger utility */ + private static final Logger logger = LoggerFactory.getLogger(Digraph.class); + + //~ Instance fields -------------------------------------------------------- + /** Name of this instance, meant to ease debugging */ + private final String name; + + /** Related Vertex (sub)class, to create vertices of the proper type */ + private final Class vertexClass; + + /** All current Vertices of the graph, handled by a map: Id -> Vertex */ + private final ConcurrentHashMap vertices = new ConcurrentHashMap<>(); + + /** Global id to uniquely identify a vertex */ + private final AtomicInteger globalVertexId = new AtomicInteger(0); + + //~ Constructors ----------------------------------------------------------- + //--------------// + // BasicDigraph // + //--------------// + /** + * Construct a BasicDigraph object. + * + * @param name the distinguished name for this instance + * @param vertexClass precise class to be used when instantiating vertices + */ + public BasicDigraph (String name, + Class vertexClass) + { + if (vertexClass == null) { + throw new IllegalArgumentException("null vertex class"); + } + + this.name = name; + this.vertexClass = vertexClass; + } + + //~ Methods ---------------------------------------------------------------- + //-----------// + // addVertex // + //-----------// + @Override + public void addVertex (V vertex) + { + if (vertex == null) { + throw new IllegalArgumentException("Cannot add a null vertex"); + } + + vertex.setGraph(this); // Unchecked + vertex.setId(globalVertexId.incrementAndGet()); // Atomic increment + vertices.put(vertex.getId(), vertex); // Atomic insertion + } + + //--------------// + // createVertex // + //--------------// + @Override + public V createVertex () + { + V vertex; + + try { + vertex = vertexClass.newInstance(); + addVertex(vertex); + + return vertex; + } catch (NullPointerException ex) { + throw new RuntimeException( + "BasicDigraph cannot create vertex, vertexClass not set"); + } catch (InstantiationException ex) { + throw new RuntimeException( + "Cannot createVertex with an abstract class or interface: " + + vertexClass); + } catch (IllegalAccessException ex) { + throw new RuntimeException(ex); + } + } + + //------// + // dump // + //------// + @Override + public void dump (String title) + { + if (title != null) { + System.out.println(title); + } + + System.out.println(this); + + for (V vertex : getVertices()) { + vertex.dump(); + } + } + + //-----------------// + // getLastVertexId // + //-----------------// + @Override + public int getLastVertexId () + { + return globalVertexId.get(); + } + + //---------// + // getName // + //---------// + @Override + public String getName () + { + return name; + } + + //---------------// + // getVertexById // + //---------------// + @Override + public V getVertexById (int id) + { + return vertices.get(id); + } + + //----------------// + // getVertexCount // + //----------------// + @Override + public int getVertexCount () + { + return vertices.size(); + } + + //-------------// + // getVertices // + //-------------// + @Override + public Collection getVertices () + { + return Collections.unmodifiableCollection(vertices.values()); + } + + //--------------// + // removeVertex // + //--------------// + @Override + public void removeVertex (V vertex) + { + logger.debug("remove {}", vertex); + + if (vertex == null) { + throw new IllegalArgumentException( + "Trying to remove a null vertex"); + } + + if (vertices.remove(vertex.getId()) == null) { // Atomic removal + throw new RuntimeException( + "Trying to remove an unknown vertex: " + vertex); + } + } + + //---------------// + // restoreVertex // + //---------------// + @Override + public void restoreVertex (V vertex) + { + vertices.put(vertex.getId(), vertex); // Atomic insertion + } + + //----------// + // toString // + //----------// + @Override + public String toString () + { + StringBuilder sb = new StringBuilder(); + sb.append("{").append(getClass().getSimpleName()); + sb.append(internalsString()); + sb.append("}"); + + return sb.toString(); + } + + //-----------------// + // internalsString // + //-----------------// + /** + * Return the string of the internals of this class, typically for + * inclusion in a toString. + * The overriding methods should comply with the following rule: + * return either a totally empty string, or a string that begins with + * a " " followed by some content. + * + * @return the string of internals + */ + protected String internalsString () + { + StringBuilder sb = new StringBuilder(25); + + sb.append("#").append(name); + + sb.append(" vertices=").append(getVertexCount()); + + if (this.getClass().getName().equals(Digraph.class.getName())) { + sb.append("}"); + } + + return sb.toString(); + } +} diff --git a/src/main/omr/graph/BasicVertex.java b/src/main/omr/graph/BasicVertex.java new file mode 100644 index 0000000..41fc684 --- /dev/null +++ b/src/main/omr/graph/BasicVertex.java @@ -0,0 +1,376 @@ +//----------------------------------------------------------------------------// +// // +// B a s i c V e r t e x // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.graph; + +import net.jcip.annotations.NotThreadSafe; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.List; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlAttribute; + +/** + * Class {@code BasicVertex} is a basic implementation of {@link Vertex}. + * + *

NOTA: This plain Vertex type has no room for user-defined data, if such + * feature is needed then a proper subtype of BasicVertex should be used. + * + *

This class is not thread-safe, because it is not intended to be used by + * several threads simultaneously. However, the graph structure which contains + * instances of vertices is indeed thread-safe. + * + * @param type for enclosing digraph precise subtype + * @param type for Vertex precise subtype + * + * @author Hervé Bitteur + */ +@NotThreadSafe +@XmlAccessorType(XmlAccessType.NONE) +public abstract class BasicVertex> + implements Vertex +{ + //~ Static fields/initializers --------------------------------------------- + + /** Usual logger utility */ + private static final Logger logger = LoggerFactory.getLogger(BasicVertex.class); + + //~ Instance fields -------------------------------------------------------- + /** + * Unique vertex Id (for debugging mainly) + */ + private int id; + + /** + * Incoming edges from other vertices + */ + protected final List sources = new ArrayList<>(); + + /** + * Outgoing edges to other vertices + */ + protected final List targets = new ArrayList<>(); + + /** + * Containing graph + */ + protected D graph; + + /** + * Sequence of views created on this vertex. Index in the sequence is + * important, since this sequence is kept parallel to the sequence of views + * on the containing graph. + */ + protected List views; + + //~ Constructors ----------------------------------------------------------- + //--------// + // Vertex // + //--------// + /** + * Create a Vertex. + */ + protected BasicVertex () + { + logger.debug("new vertex"); + } + + //--------// + // Vertex // + //--------// + /** + * Create a Vertex in a graph. + * + * @param graph The containing graph where this vertex is to be hosted + */ + protected BasicVertex (D graph) + { + logger.debug("new vertex in graph {}", graph); + + graph.addVertex(this); // Compiler warning here + } + + //~ Methods ---------------------------------------------------------------- + //-----------// + // addTarget // + //-----------// + @Override + public void addTarget (V target) + { + logger.debug("adding edge from {} to {}", this, target); + + // Assert we have real target + if (target == null) { + throw new IllegalArgumentException( + "Cannot add an edge to a null target"); + } + + // Assert this vertex and target vertex belong to the same graph + if (this.getGraph() != target.getGraph()) { + throw new RuntimeException( + "An edge can link vertices of the same graph only"); + } + + // Avoid duplicates + getTargets().remove(target); + target.getSources().remove((V) this); + + targets.add(target); + target.getSources().add((V) this); + } + + //---------// + // addView // + //---------// + @Override + public void addView (VertexView view) + { + getViews().add(view); + } + + //------------// + // clearViews // + //------------// + @Override + public void clearViews () + { + getViews().clear(); + } + + //--------// + // delete // + //--------// + @Override + public void delete () + { + try { + logger.debug("deleting vertex {}", this); + + // Remove in vertices of the vertex + for (V source : new ArrayList<>(getSources())) { + source.removeTarget((V) this, false); + } + + // Remove out vertices of the vertex + for (V target : new ArrayList<>(getTargets())) { + removeTarget(target, false); + } + + // Remove from graph + graph.removeVertex(this); + } catch (Exception ex) { + logger.error("Error deleting " + this, ex); + } + } + + //------// + // dump // + //------// + @Override + public void dump () + { + StringBuilder sb = new StringBuilder(); + + // The vertex + sb.append(String.format("%s%n", this)); + + // The in edges + for (V vertex : sources) { + sb.append(String.format(" edge from %s%n", vertex)); + } + + // The out edges + for (V vertex : targets) { + sb.append(String.format(" edge to %s%n", vertex)); + } + + logger.info(sb.toString()); + } + + //----------// + // getGraph // + //----------// + @Override + public D getGraph () + { + return graph; + } + + //-------// + // getId // + //-------// + @Override + public int getId () + { + return id; + } + + //-------------// + // getInDegree // + //-------------// + @Override + public int getInDegree () + { + return sources.size(); + } + + //--------------// + // getOutDegree // + //--------------// + @Override + public int getOutDegree () + { + return targets.size(); + } + + //------------// + // getSources // + //------------// + @Override + public List getSources () + { + return sources; + } + + //------------// + // getTargets // + //------------// + @Override + public List getTargets () + { + return targets; + } + + //---------// + // getView // + //---------// + @Override + public VertexView getView (int index) + { + return getViews().get(index); + } + + //---------------// + // getViewsCount // + //---------------// + @Override + public int getViewsCount () + { + return getViews().size(); + } + + //--------------// + // removeTarget // + //--------------// + @Override + public void removeTarget (V target, + boolean strict) + { + // Assert we have real target + if (target == null) { + throw new IllegalArgumentException( + "Cannot remove an edge to a null target"); + } + + if (!this.targets.remove(target) && strict) { + throw new RuntimeException( + "Attempting to remove non-existing edge between " + this + + " and " + target); + } + + if (!target.getSources().remove((V) this) && strict) { + throw new RuntimeException( + "Attempting to remove non-existing edge between " + this + + " and " + target); + } + } + + //----------// + // setGraph // + //----------// + /** + * (package access from graph) + */ + @Override + public void setGraph (D graph) + { + this.graph = graph; + } + + //-------// + // setId // + //-------// + @XmlAttribute + @Override + public void setId (int id) + { + this.id = id; + } + + //----------// + // toString // + //----------// + @Override + public String toString () + { + StringBuilder sb = new StringBuilder(); + + sb.append("{").append(getClass().getSimpleName()).append("#").append(id); + + sb.append(internalsString()); + + sb.append("}"); + + return sb.toString(); + } + + //-----------------// + // internalsString // + //-----------------// + /** + * Return the string of the internals of this class, typically for inclusion + * in a toString. The overriding methods, if any, should return a string + * that begins with a " " followed by some content. + * + * @return the string of internals + */ + protected String internalsString () + { + StringBuilder sb = new StringBuilder(100); + + sb.append(" ").append(getInDegree()); + sb.append("/").append(getOutDegree()); + + return sb.toString(); + } + + //----------// + // getViews // + //----------// + /** + * Report the sequence of the related views, lazily created. + * + * @return the views collection (perhaps empty, but not null) + */ + private List getViews () + { + if (views == null) { + views = new ArrayList<>(); + } + + return views; + } +} diff --git a/src/main/omr/graph/Digraph.java b/src/main/omr/graph/Digraph.java new file mode 100644 index 0000000..d70c490 --- /dev/null +++ b/src/main/omr/graph/Digraph.java @@ -0,0 +1,121 @@ +//----------------------------------------------------------------------------// +// // +// D i g r a p h // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.graph; + +import java.util.Collection; + +/** + * Class {@code Digraph} handles a directed graph, a structure + * containing an homogeneous collection of instances of Vertex + * (or a collection of homogeneous types derived from Vertex), + * potentially linked by directed edges. + *

+ * + *

Vertices can exist in isolation, but an edge can exist only from a vertex + * to another vertex. Thus, removing a vertex implies removing all its incoming + * and outgoing edges. + * + *

NOTA: Since we have no data to carry in edges, there is no + * {@code Edge} type per se, links between vertices are implemented simply + * by Lists of Vertex. + * + * @param precise type for digraph (which is pointed back by vertex) + * @param precise type for vertices handled by this digraph + * + * @author Hervé Bitteur + */ +public interface Digraph, V extends Vertex> +{ + //~ Methods ---------------------------------------------------------------- + + /** + * Add a vertex in the graph, the vertex is being assigned a + * unique id by the graph. + * (package access from {@link Vertex}) + * Made public just for access from glyph Verifier + * + * @param vertex the newly created vertex + */ + void addVertex (V vertex); + + /** + * Create a new vertex in the graph, using the provided vertex + * class. + * + * @return the vertex created + */ + V createVertex (); + + /** + * A dump of the graph content, vertex by vertex + * + * @param title The title to be printed before the dump, or null + */ + void dump (String title); + + /** + * Give access to the last id assigned to a vertex in this graph. + * This may be greater than the number of vertices currently in the graph, + * because of potential deletion of vertices (a Vertex Id is never reused). + * + * @return id of the last vertex created + * @see #getVertexCount + */ + int getLastVertexId (); + + /** + * Report the name assigned to this graph instance + * + * @return the readable name + */ + String getName (); + + /** + * Retrieve a vertex knowing its id + * + * @param id the vertex id + * @return the vertex found, or null + */ + V getVertexById (int id); + + /** + * Give the number of vertices currently in the graph. + * + * @return the number of vertices + * @see #getLastVertexId + */ + int getVertexCount (); + + /** + * Export an unmodifiable and non-sorted collection of vertices of + * the graph + * + * @return the unmodifiable collection of vertices + */ + Collection getVertices (); + + /** + * (package access from Vertex) to remove the vertex from the + * graph, the removed vertex will now be stored in the oldVertices + * map. + * + * @param vertex the vertex to be removed + */ + void removeVertex (V vertex); + + /** + * Restore an old vertex + * + * @param vertex the old vertex to restore + */ + void restoreVertex (V vertex); +} diff --git a/src/main/omr/graph/DigraphView.java b/src/main/omr/graph/DigraphView.java new file mode 100644 index 0000000..e000019 --- /dev/null +++ b/src/main/omr/graph/DigraphView.java @@ -0,0 +1,36 @@ +//----------------------------------------------------------------------------// +// // +// D i g r a p h V i e w // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.graph; + +import java.awt.Graphics2D; + +/** + * Interface {@code DigraphView} defines what is needed to view a graph. + * + * @author Hervé Bitteur + */ +public interface DigraphView +{ + //~ Methods ---------------------------------------------------------------- + + /** + * Refresh the display + */ + void refresh (); + + /** + * Render the whole graph view + * + * @param g the graphics context + */ + void render (Graphics2D g); +} diff --git a/src/main/omr/graph/Vertex.java b/src/main/omr/graph/Vertex.java new file mode 100644 index 0000000..2067f6e --- /dev/null +++ b/src/main/omr/graph/Vertex.java @@ -0,0 +1,146 @@ +//----------------------------------------------------------------------------// +// // +// V e r t e x // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.graph; + +import java.util.List; + +/** + * Interface {@code Vertex} encapsulates a Vertex (or Node) in a + * directed graph. + * Any vertex can have incoming edges from other vertices and outgoing + * edges to other vertices. + * + *

The Vertex can have a list of related {@code VertexView}'s. + * All the vertices in the graph have parallel lists of {@code VertexView}'s as + * the Digraph itself which has a parallel list of {@code DigraphView}'s. + * + * @param type for enclosing digraph precise subtype + * @param type for Vertex precise subtype + * + * @author Hervé Bitteur + */ +public interface Vertex> +{ + //~ Methods ---------------------------------------------------------------- + + /** + * Create an edge between this vertex and the target vertax + * + * @param target arrival vertex + */ + public void addTarget (V target); + + /** + * Add a related view of this vertex + * + * @param view the view to be linked + */ + public void addView (VertexView view); + + /** + * Get rid of all views for this vertex + */ + public void clearViews (); + + /** + * Delete this vertex. + * This implies also the removal of all its incoming and outgoing edges. + */ + public void delete (); + + /** + * Prints on standard output a detailed information about this + * vertex. + */ + public void dump (); + + /** + * Report the containing graph of this vertex + * + * @return the containing graph + */ + public D getGraph (); + + /** + * Report the unique Id (within the containing graph) of this + * vertex. + * + * @return the id + */ + public int getId (); + + /** + * Return how many incoming edges we have + * + * @return the number of incomings + */ + public int getInDegree (); + + /** + * Return the number of edges outgoing from this vertex + * + * @return the number of outgoings + */ + public int getOutDegree (); + + /** + * An access to incoming vertices + * + * @return the incoming vertices + */ + public List getSources (); + + /** + * Return an access to the outgoing vertices of this vertex + * + * @return the outgoing vertices + */ + public List getTargets (); + + /** + * Report the view at given index + * + * @param index index of the desired view + * @return the desired view + */ + public VertexView getView (int index); + + /** + * Report the current number of views on this Vertex + * + * @return the current number of views + */ + public int getViewsCount (); + + /** + * Remove an edge between this vertex and a target vertex + * + * @param target arrival vertex + * @param strict throw RuntimeException if the edge does not exist + */ + public void removeTarget (V target, + boolean strict); + + /** + * Assign the containing graph of this vertex + * + * @param graph The hosting graph + */ + public void setGraph (D graph); + + /** + * Assign a new Id (for expert use only) + * + * @param id The assigned id + */ + public void setId (int id); +} diff --git a/src/main/omr/graph/VertexView.java b/src/main/omr/graph/VertexView.java new file mode 100644 index 0000000..6c58849 --- /dev/null +++ b/src/main/omr/graph/VertexView.java @@ -0,0 +1,43 @@ +//----------------------------------------------------------------------------// +// // +// V e r t e x V i e w // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.graph; + +import java.awt.Graphics; +import java.awt.Rectangle; + +/** + * Interface {@code VertexView} defines the interface needed to handle + * the rendering of a vertex. + * + * @author Hervé Bitteur + */ +public interface VertexView +{ + //~ Methods ---------------------------------------------------------------- + + /** + * Return the display rectangle used by the rendering of the vertex + * + * @return the bounding rectangle in the display space + */ + Rectangle getBounds (); + + /** + * Render the vertex + * + * @param g the graphics context + * @param drawBorders should vertex borders be drawn + * @return true if actually rendered, i.e. is displayed + */ + boolean render (Graphics g, + boolean drawBorders); +} diff --git a/src/main/omr/graph/package.html b/src/main/omr/graph/package.html new file mode 100644 index 0000000..a767e3f --- /dev/null +++ b/src/main/omr/graph/package.html @@ -0,0 +1,17 @@ + + + + + + Package omr.graph + + + +

+ This package implements a generic directed graph, made of vertices + linked by directed edges. +

+ + + diff --git a/src/main/omr/grid/BarAlignment.java b/src/main/omr/grid/BarAlignment.java new file mode 100644 index 0000000..3cb69f3 --- /dev/null +++ b/src/main/omr/grid/BarAlignment.java @@ -0,0 +1,199 @@ +//----------------------------------------------------------------------------// +// // +// B a r A l i g n m e n t // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.grid; + +import omr.glyph.facets.Glyph; + +import omr.sheet.Sheet; + +import omr.util.Navigable; + +import java.awt.geom.Point2D; + +/** + * Class {@code BarAlignment} is used to collect all bar lines within a + * system which should be vertically aligned (typically at the end of a + * measure), and to check for proper alignment. + * + * @author Hervé Bitteur + */ +public class BarAlignment +{ + //~ Instance fields -------------------------------------------------------- + + /** Related sheet. */ + @Navigable(false) + private final Sheet sheet; + + /** Used to flag a manually defined alignment. */ + private boolean manual; + + /** Vertical sequence of bars intersections with staves. */ + private final StickIntersection[] inters; + + //~ Constructors ----------------------------------------------------------- + /** + * Creates a new BarAlignment object. + * + * @param sheet the related sheet + * @param staffCount the number of staves to check for consistency + */ + public BarAlignment (Sheet sheet, + int staffCount) + { + this.sheet = sheet; + inters = new StickIntersection[staffCount]; + } + + //~ Methods ---------------------------------------------------------------- + //----------// + // addInter // + //----------// + /** + * Record an intersection for the given (staff) index + * + * @param index the (staff) index + * @param inter the intersection to record + */ + public void addInter (int index, + StickIntersection inter) + { + inters[index] = inter; + } + + //----------// + // distance // + //----------// + /** + * Compute the horizontal distance between the provided intersection and + * this alignment, taking the global sheet skew into account. + * + * @param index the provided (staff) index + * @param inter the provided intersection + * @return the relative abscissa distance from this alignment to the + * provided intersection + */ + public Double distance (int index, + StickIntersection inter) + { + for (int i = index - 1; i >= 0; i--) { + StickIntersection si = inters[i]; + + if (si != null) { + Point2D dskSi = sheet.getSkew() + .deskewed(new Point2D.Double(si.x, si.y)); + Point2D dskIt = sheet.getSkew() + .deskewed( + new Point2D.Double(inter.x, inter.y)); + + return dskIt.getX() - dskSi.getX(); + } + } + + // Could not measure anything + return null; + } + + //----------------// + // getFilledCount // + //----------------// + /** + * Report the number of intersections found for this bar alignment + * + * @return the percentage filled + */ + public int getFilledCount () + { + int cells = 0; + + for (StickIntersection inter : inters) { + if (inter != null) { + cells++; + } + } + + return cells; + } + + //------------------// + // getIntersections // + //------------------// + /** + * Report the array of intersections found, one cell per staff. + * + * @return the intersections found (with null array cells for missing + * intersections) + */ + public StickIntersection[] getIntersections () + { + return inters; + } + + //----------// + // isManual // + //----------// + /** + * Report whether this alignment is manually defined or not. + * + * @return the manual flag + */ + public boolean isManual () + { + return manual; + } + + //-----------// + // setManual // + //-----------// + /** + * Flag this alignment as a manual one. + * + * @param manual the manual flag to set + */ + public void setManual (boolean manual) + { + this.manual = manual; + } + + //----------// + // toString // + //----------// + @Override + public String toString () + { + StringBuilder sb = new StringBuilder("{"); + sb.append(getClass().getSimpleName()); + + sb.append(" ") + .append(getFilledCount()) + .append("/") + .append(inters.length); + + for (int i = 0; i < inters.length; i++) { + sb.append(" ") + .append(i) + .append(":"); + + if (inters[i] != null) { + Glyph stick = inters[i].getStickAncestor(); + sb.append("#") + .append(stick.getId()); + } else { + sb.append("null"); + } + } + + sb.append("}"); + + return sb.toString(); + } +} diff --git a/src/main/omr/grid/BarInfo.java b/src/main/omr/grid/BarInfo.java new file mode 100644 index 0000000..447139f --- /dev/null +++ b/src/main/omr/grid/BarInfo.java @@ -0,0 +1,101 @@ +//----------------------------------------------------------------------------// +// // +// B a r I n f o // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.grid; + +import omr.glyph.facets.Glyph; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; + +/** + * Class {@code BarInfo} records the physical information about a bar + * line, used especially as a vertical limit for a staff or system. + * + * @author Hervé Bitteur + */ +public class BarInfo +{ + //~ Instance fields -------------------------------------------------------- + + /** Composing sticks, ordered by their relative abscissa. */ + private List sticks; + + //~ Constructors ----------------------------------------------------------- + //---------// + // BarInfo // + //---------// + /** + * Creates a new BarInfo object. + * + * @param sticks one or several physical bars, from left to right + */ + public BarInfo (Glyph... sticks) + { + this(Arrays.asList(sticks)); + } + + //---------// + // BarInfo // + //---------// + /** + * Creates a new BarInfo object. + * + * @param sticks one or several physical bars, from left to right + */ + public BarInfo (Collection sticks) + { + setSticks(sticks); + } + + //~ Methods ---------------------------------------------------------------- + //--------------------// + // getSticksAncestors // + //--------------------// + public List getSticksAncestors () + { + List list = new ArrayList<>(sticks.size()); + + for (Glyph stick : sticks) { + list.add(stick.getAncestor()); + } + + return list; + } + + //-----------// + // setSticks // + //-----------// + public final void setSticks (Collection sticks) + { + this.sticks = new ArrayList<>(sticks); // Copy + } + + //----------// + // toString // + //----------// + @Override + public String toString () + { + StringBuilder sb = new StringBuilder("{BarInfo"); + + for (Glyph stick : sticks) { + sb.append(" #") + .append(stick.getAncestor().getId()); + } + + sb.append("}"); + + return sb.toString(); + } +} diff --git a/src/main/omr/grid/BarsRetriever.java b/src/main/omr/grid/BarsRetriever.java new file mode 100644 index 0000000..6c2cd71 --- /dev/null +++ b/src/main/omr/grid/BarsRetriever.java @@ -0,0 +1,1868 @@ +//----------------------------------------------------------------------------// +// // +// B a r s R e t r i e v e r // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.grid; + +import omr.Main; + +import omr.constant.Constant; +import omr.constant.ConstantSet; + +import omr.glyph.Glyphs; +import omr.glyph.facets.Glyph; +import omr.glyph.ui.NestView; + +import omr.lag.BasicLag; +import omr.lag.JunctionRatioPolicy; +import omr.lag.Lag; +import omr.lag.Section; +import omr.lag.SectionsBuilder; + +import omr.math.Barycenter; +import omr.math.BasicLine; +import omr.math.Line; +import omr.math.LineUtil; + +import static omr.run.Orientation.*; +import omr.run.RunsTable; + +import omr.sheet.BarsChecker; +import omr.sheet.Scale; +import omr.sheet.Sheet; +import omr.sheet.Skew; +import omr.sheet.SystemInfo; + +import omr.step.StepException; + +import omr.ui.Colors; +import omr.ui.util.UIUtil; + +import omr.util.HorizontalSide; +import static omr.util.HorizontalSide.*; +import omr.util.VipUtil; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.awt.Color; +import java.awt.Graphics2D; +import java.awt.Stroke; +import java.awt.geom.Line2D; +import java.awt.geom.Point2D; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.SortedSet; +import java.util.TreeMap; +import java.util.TreeSet; + +/** + * Class {@code BarsRetriever} focuses on the retrieval of vertical + * barlines. + * Barlines are used to determine the side limits of staves and, most + * importantly, the gathering of staves into systems. + * The other barlines are used to determine parts and measures. + * + * @author Hervé Bitteur + */ +public class BarsRetriever + implements NestView.ItemRenderer +{ + //~ Static fields/initializers --------------------------------------------- + + /** Specific application parameters. */ + private static final Constants constants = new Constants(); + + /** Usual logger utility. */ + private static final Logger logger = LoggerFactory.getLogger(BarsRetriever.class); + + //~ Instance fields -------------------------------------------------------- + // + /** Related sheet. */ + private final Sheet sheet; + + /** Scale-dependent constants for vertical stuff. */ + private final Parameters params; + + /** Lag of vertical runs. */ + private Lag vLag; + + /** Related staff manager. */ + private final StaffManager staffManager; + + /** Long vertical filaments found, non sorted. */ + private final List filaments = new ArrayList<>(); + + /** Intersections between staves and bar sticks. */ + private final Map crossings = new TreeMap<>(StaffInfo.byId); + + /** + * System tops. + * For each staff, gives the staff that starts the containing system + */ + private Integer[] systemTops; + + /** + * Part tops. + * For each staff, gives the staff that starts the containing part + */ + private Integer[] partTops; + + //~ Constructors ----------------------------------------------------------- + // + //---------------// + // BarsRetriever // + //---------------// + /** + * Retrieve the frames of all staff lines. + * + * @param sheet the sheet to process + */ + public BarsRetriever (Sheet sheet) + { + this.sheet = sheet; + + // Scale-dependent parameters + params = new Parameters(sheet.getScale()); + + // Companions + staffManager = sheet.getStaffManager(); + } + + //~ Methods ---------------------------------------------------------------- + // + //----------// + // buildLag // + //----------// + /** + * Build the underlying lag, out of the provided runs table. + * This method must be called before building info. + * + * @param vertTable the provided table of vertical runs + */ + public void buildLag (RunsTable vertTable) + { + vLag = new BasicLag("vLag", VERTICAL); + + SectionsBuilder sectionsBuilder = new SectionsBuilder( + vLag, + new JunctionRatioPolicy(params.maxLengthRatio)); + sectionsBuilder.createSections(vertTable); + + sheet.setVerticalLag(vLag); + + // Debug sections VIPs + for (int id : params.vipSections) { + Section sect = vLag.getVertexById(id); + + if (sect != null) { + sect.setVip(); + } + } + } + + //-------------// + // renderItems // + //-------------// + /** + * Render the filaments and their ending tangents if so desired. + * + * @param g graphics context + */ + @Override + public void renderItems (Graphics2D g) + { + if (!constants.showVerticalLines.isSet()) { + return; + } + + final Stroke oldStroke = UIUtil.setAbsoluteStroke(g, 1f); + final Color oldColor = g.getColor(); + g.setColor(Colors.ENTITY_MINOR); + + // Draw filaments + for (Glyph filament : filaments) { + filament.renderLine(g); + } + + // Draw tangent at each ending point (using max coord gap)? + if (constants.showTangents.isSet()) { + g.setColor(Colors.TANGENT); + + double dy = sheet.getScale() + .toPixels(constants.maxCoordGap); + + for (Glyph glyph : filaments) { + Point2D p = glyph.getStartPoint(VERTICAL); + double derivative = (glyph instanceof Filament) + ? ((Filament) glyph).slopeAt(p.getY(), + VERTICAL) + : glyph.getInvertedSlope(); + g.draw(new Line2D.Double(p.getX(), p.getY(), + p.getX() - (derivative * dy), + p.getY() - dy)); + p = glyph.getStopPoint(VERTICAL); + derivative = (glyph instanceof Filament) + ? ((Filament) glyph).slopeAt(p.getY(), VERTICAL) + : glyph.getInvertedSlope(); + g.draw(new Line2D.Double(p.getX(), p.getY(), + p.getX() + (derivative * dy), + p.getY() + dy)); + } + } + + g.setStroke(oldStroke); + g.setColor(oldColor); + } + + //---------------------// + // retrieveMeasureBars // + //---------------------// + /** + * Use long vertical sections to retrieve the barlines that define + * measures. + * We first filter the bar candidates in a less rough manner now that we + * have precise values for staff lines coordinates. + * Then, we use consistency checks within the same system, where measure + * barlines should be aligned vertically. + * + *
+     * retrieveMeasureBars()
+     *    +  recheckBarCandidates()       // Strict barSticks checking
+     *
+     *    +  (per system)
+     *       + checkBarsAlignment(system) // Check bars consistency w/i system
+     *
+     *    +  retrievePartTops()           // Retrieve parts top staves
+     *
+     *    +  (per system)
+     *       + refineBarsEndings(system)  // Refine endings for each bar line
+     * 
+ */ + public void retrieveMeasureBars () + { + // Strict barSticks checking + recheckBarCandidates(); + + // Check bars consistency within the same system + for (SystemInfo system : sheet.getSystems()) { + checkBarsAlignment(system); + } + + // We already have systemTop for each staff + // let's try to define partTop for each staff + partTops = retrievePartTops(); + + logger.info("{}Parts top staff ids: {}", + sheet.getLogPrefix(), Arrays.toString(partTops)); + + // Refine ending points for each bar line + for (SystemInfo system : sheet.getSystems()) { + refineBarsEndings(system); + } + } + + //--------------------// + // retrieveSystemBars // + //--------------------// + /** + * Use the long vertical sections to retrieve the barlines that + * define the limits of systems and staves. + * + *
+     * retrieveSystemBars()
+     *    +  retrieveMajorBars()
+     *       +  buildVerticalFilaments()       // Build vertical filaments
+     *       +  retrieveBarCandidates()        // Retrieve initial barline candidates
+     *       +  populateCrossings()            // Assign bar candidates to intersected staves
+     *       +  retrieveStaffSideBars()        // Retrieve left staff bars and right staff bars
+     *
+     *    +  buildSystems()
+     *       +  retrieveSystemTops()           // Retrieve top staves (they start systems)
+     *       +  createSystems()                // Create system frames using top staves
+     *       +  mergeSystems()                 // Merge of systems as much as possible
+     *
+     *    +  adjustSides()                     // Adjust sides for systems, staves & lines
+     *       +  (per system)
+     *          +  (per side)
+     *             + adjustSystemLimit(system, side) // Adjust system limit
+     *          +  adjustStaffLines(system)    // Adjust staff lines to system limits
+     * 
+ * + * @throws StepException raised if processing must stop + */ + public void retrieveSystemBars (Collection oldGlyphs, + Collection newGlyphs) + throws StepException + { + try { + // Retrieve major barSticks (for system & staves limits) + retrieveMajorBars(oldGlyphs, newGlyphs); + } catch (Exception ex) { + logger.warn(sheet.getLogPrefix() + + "BarsRetriever cannot retrieveBars", ex); + } + + try { + // Detect systems of staves aggregated via barlines + buildSystems(); + } catch (Exception ex) { + logger.warn(sheet.getLogPrefix() + + "BarsRetriever cannot retrieveSystems", ex); + } + + // Adjust precise horizontal sides for systems, staves & lines + adjustSides(); + } + + //------------------// + // adjustStaffLines // + //------------------// + /** + * Staff by staff, align the lines endings with the system limits, + * and check the intermediate line points. + * Package access meant for LinesRetriever companion. + * + * @param system the system to process + */ + void adjustStaffLines (SystemInfo system) + { + for (StaffInfo staff : system.getStaves()) { + logger.debug("{}", staff); + + // Adjust left and right endings of each line in the staff + for (LineInfo l : staff.getLines()) { + FilamentLine line = (FilamentLine) l; + line.setEndingPoints(getLineEnding(system, staff, line, LEFT), + getLineEnding(system, staff, line, RIGHT)); + } + + // Insert line intermediate points, if so needed + List fils = new ArrayList<>(); + + for (LineInfo l : staff.getLines()) { + FilamentLine line = (FilamentLine) l; + fils.add(line.fil); + } + + for (LineInfo l : staff.getLines()) { + FilamentLine line = (FilamentLine) l; + line.fil.fillHoles(fils); + } + } + } + + //------------------// + // adjustSystemBars // + //------------------// + /** + * Adjust start and stop points of system side barSticks. + */ + void adjustSystemBars () + { + for (SystemInfo system : sheet.getSystems()) { + try { + for (HorizontalSide side : HorizontalSide.values()) { + Object limit = system.getLimit(side); + + if (limit != null) { + // Determine first point of barline + StaffInfo firstStaff = system.getFirstStaff(); + Point2D pStart = firstStaff.getFirstLine(). + getEndPoint(side); + + // Determine last point of barline + StaffInfo lastStaff = system.getLastStaff(); + Point2D pStop = lastStaff.getLastLine(). + getEndPoint(side); + + // [Dirty programming, sorry] + if (limit instanceof Glyph) { + ((Glyph) limit).setEndingPoints(pStart, pStop); + } + } + } + } catch (Exception ex) { + logger.warn("BarsRetriever can't adjust side bars of " + + system.idString(), ex); + } + } + } + + //----------------------// + // getSideBarlineGlyphs // + //----------------------// + /** + * Report the set of all sticks that are actually part of the staff + * side barlines (left or right side). + * + * @return the collection of used barline sticks + */ + Set getSideBarlineGlyphs () + { + Set sticks = new HashSet<>(); + + for (StaffInfo staff : staffManager.getStaves()) { + for (HorizontalSide side : HorizontalSide.values()) { + BarInfo bar = staff.getBar(side); + + if (bar != null) { + sticks.addAll(bar.getSticksAncestors()); + } + } + } + + return sticks; + } + + //--------// + // isLong // + //--------// + boolean isLong (Glyph stick) + { + return (stick != null) + && (stick.getLength(VERTICAL) >= params.minLongLength); + } + + //--------// + // isLong // + //--------// + boolean isLong (BarInfo bar) + { + if (bar != null) { + for (Glyph stick : bar.getSticksAncestors()) { + if (isLong(stick)) { + return true; + } + } + } + + return false; + } + + //-------------// + // adjustSides // + //-------------// + /** + * Adjust precise sides for systems, staves & lines. + */ + private void adjustSides () + { + for (SystemInfo system : sheet.getSystems()) { + try { + for (HorizontalSide side : HorizontalSide.values()) { + // Determine the side limit of the system + adjustSystemLimit(system, side); + } + + if (logger.isDebugEnabled()) { + logger.debug("System#{} left:{} right:{}", system.getId(), + system.getLimit(LEFT).getClass() + .getSimpleName(), + system.getLimit(RIGHT).getClass() + .getSimpleName()); + } + + // Use system limits to adjust staff lines + adjustStaffLines(system); + } catch (Exception ex) { + logger.warn("BarsRetriever cannot adjust system#" + + system.getId(), ex); + } + } + } + + //-------------------// + // adjustSystemLimit // + //-------------------// + /** + * Adjust the limit on the desired side of the provided system and + * store the chosen limit (whatever it is) in the system itself. + * + * @param system the system to process + * @param side the desired side + * + * @see SystemInfo#getLimit(HorizontalSide) + */ + private void adjustSystemLimit (SystemInfo system, + HorizontalSide side) + { + Glyph drivingStick = null; + + // Do we have a bar embracing the whole system? + BarInfo bar = retrieveSystemBar(system, side); + + if (bar != null) { + // We use the heaviest stick among the system-embracing bar sticks + SortedSet allSticks = new TreeSet<>( + Glyph.byReverseWeight); + + for (Glyph stick : bar.getSticksAncestors()) { + Point2D start = stick.getStartPoint(VERTICAL); + StaffInfo topStaff = staffManager.getStaffAt(start); + Point2D stop = stick.getStopPoint(VERTICAL); + StaffInfo botStaff = staffManager.getStaffAt(stop); + + if ((topStaff == system.getFirstStaff()) + && (botStaff == system.getLastStaff())) { + allSticks.add(stick); + } + } + + if (!allSticks.isEmpty()) { + drivingStick = allSticks.first(); + + logger.debug("System#{} {} drivingStick: {}", + system.getId(), side, drivingStick); + + // Polish long driving stick, if needed + if (isLong(drivingStick) && drivingStick instanceof Filament) { + ((Filament) drivingStick).polishCurvature(); + } + + // Remember approximate limit abscissa for each staff + for (StaffInfo staff : system.getStaves()) { + Point2D inter = staff.intersection(drivingStick); + staff.setAbscissa(side, inter.getX()); + } + + system.setLimit(side, drivingStick); + } + } + + if (drivingStick == null) { + // We fall back using some centroid & slope + // TODO: This algorithm could be refined + Barycenter bary = new Barycenter(); + + for (StaffInfo staff : system.getStaves()) { + double x = extendStaffAbscissa(staff, side); + double y = staff.getMidOrdinate(side); + bary.include(x, y); + } + + logger.debug("System#{} {} barycenter: {}", + system.getId(), side, bary); + + double slope = sheet.getSkew().getSlope(); + BasicLine line = new BasicLine(); + line.includePoint(bary.getX(), bary.getY()); + line.includePoint(bary.getX() - (100 * slope), bary.getY() + 100); + system.setLimit(side, line); + } + } + + //--------------// + // buildSystems // + //--------------// + /** + * Detect systems of staves aggregated via connecting barlines. + */ + private void buildSystems () + { + do { + // Retrieve the staves that start systems + if (systemTops == null) { + systemTops = retrieveSystemTops(); + } + + logger.info("{}Systems top staff ids: {}", sheet.getLogPrefix(), + Arrays.toString(systemTops)); + + // Create system frames using staves tops + sheet.setSystems(createSystems(systemTops)); + + // Merge of systems as much as possible + if (mergeSystems()) { + logger.info("Systems modified, rebuilding..."); + } else { + break; + } + } while (true); + } + + //------------------------// + // buildVerticalFilaments // + //------------------------// + /** + * With vertical lag sections, build vertical filaments. + * + * @throws Exception + */ + private void buildVerticalFilaments () + throws Exception + { + // Filaments factory + FilamentsFactory factory = new FilamentsFactory(sheet.getScale(), + sheet.getNest(), + VERTICAL, + Filament.class); + + // Factory parameters adjustment + factory.setMaxSectionThickness(constants.maxSectionThickness); + factory.setMaxFilamentThickness(constants.maxFilamentThickness); + factory.setMaxCoordGap(constants.maxCoordGap); + factory.setMaxPosGap(constants.maxPosGap); + factory.setMaxSpace(constants.maxSpace); + factory.setMaxOverlapDeltaPos(constants.maxOverlapDeltaPos); + + // Retrieve filaments out of vertical sections + filaments.addAll(factory.retrieveFilaments(vLag.getVertices(), true)); + } + + //-------------------// + // canConnectSystems // + //-------------------// + /** + * Try to merge the two provided systems into a single one. + * + * @param prevSystem the system above + * @param nextSystem the system below + * + * @return true if left barSticks have been merged + */ + private boolean canConnectSystems (SystemInfo prevSystem, + SystemInfo nextSystem) + { + List systems = sheet.getSystems(); + Skew skew = sheet.getSkew(); + + if (logger.isDebugEnabled()) { + logger.info("Checking S#{}({}) - S#{}({})", prevSystem.getId(), + prevSystem.getStaves().size(), nextSystem.getId(), + nextSystem.getStaves().size()); + } + + StaffInfo prevStaff = prevSystem.getLastStaff(); + Point2D prevStaffPt = skew.deskewed(prevStaff.getLastLine() + .getEndPoint(LEFT)); + double prevY = prevStaffPt.getY(); + BarInfo prevBar = prevStaff.getBar(LEFT); // Perhaps null + + StaffInfo nextStaff = nextSystem.getFirstStaff(); + Point2D nextStaffPt = skew.deskewed(nextStaff.getFirstLine() + .getEndPoint(LEFT)); + double nextY = nextStaffPt.getY(); + BarInfo nextBar = nextStaff.getBar(LEFT); // Perhaps null + + // Check vertical connections between barSticks + if ((prevBar != null) && (nextBar != null)) { + // case: Bar - Bar + for (Glyph prevStick : prevBar.getSticksAncestors()) { + Point2D prevPoint = skew.deskewed(prevStick.getStopPoint( + VERTICAL)); + + for (Glyph nextStick : nextBar.getSticksAncestors()) { + Point2D nextPoint = skew.deskewed(nextStick.getStartPoint( + VERTICAL)); + + // Check dx + double dx = Math.abs(nextPoint.getX() - prevPoint.getX()); + + // Check dy + double dy = Math.abs(Math.min(nextY, nextPoint.getY()) + - Math.max(prevY, prevPoint.getY())); + + logger.debug("F{}-F{} dx:{} vs {}, dy:{} vs {}", + prevStick.getId(), nextStick.getId(), + (float) dx, params.maxBarPosGap, (float) dy, + params.maxBarCoordGap); + + if ((dx <= params.maxBarPosGap) + && (dy <= params.maxBarCoordGap)) { + logger.info("Merging systems S#{}({}) - S#{}({})", + prevSystem.getId(), + prevSystem.getStaves().size(), + nextSystem.getId(), + nextSystem.getStaves().size()); + prevStick.stealSections(nextStick); + + return tryRangeConnection( + systems.subList(systems.indexOf(prevSystem), + 1 + systems.indexOf(nextSystem))); + } + } + } + } else if (prevBar != null) { + // case: Bar - noBar + Point2D prevPoint = null; + + for (Glyph prevStick : prevBar.getSticksAncestors()) { + Point2D point = skew.deskewed(prevStick.getStopPoint(VERTICAL)); + + if ((prevPoint == null) || (prevPoint.getY() < point.getY())) { + prevPoint = point; + } + } + + if ((prevPoint.getY() - prevY) > params.minBarChunkHeight) { + double dy = nextY - prevPoint.getY(); + + if (dy <= params.maxBarCoordGap) { + return tryRangeConnection(systems.subList(systems.indexOf( + prevSystem), + 1 + + systems.indexOf(nextSystem))); + } + } + } else if (nextBar != null) { + // case: NoBar - Bar + Point2D nextPoint = null; + + for (Glyph nextStick : nextBar.getSticksAncestors()) { + Point2D point = skew.deskewed(nextStick.getStartPoint(VERTICAL)); + + if ((nextPoint == null) || (nextPoint.getY() > point.getY())) { + nextPoint = point; + } + } + + if ((nextY - nextPoint.getY()) > params.minBarChunkHeight) { + double dy = nextPoint.getY() - prevY; + + if (dy <= params.maxBarCoordGap) { + return tryRangeConnection(systems.subList(systems.indexOf( + prevSystem), + 1 + + systems.indexOf(nextSystem))); + } + } + } + + return false; + } + + //--------------------// + // checkBarsAlignment // + //--------------------// + /** + * Check that, within the same system, barSticks are vertically + * aligned across all staves. + * We first build BarAlignment instances to record the bar locations and + * finally check these alignments for correctness. + * Resulting alignments are stored in the SystemInfo instance. + * + * @param system the system to process + */ + private void checkBarsAlignment (SystemInfo system) + { + List staves = system.getStaves(); + int staffCount = staves.size(); + + // Bar alignments for the system + List alignments = null; + + for (int iStaff = 0; iStaff < staves.size(); iStaff++) { + StaffInfo staff = staves.get(iStaff); + + IntersectionSequence staffCrossings = crossings.get(staff); + + // logger.info("System#" + system.getId() +" Staff#" + staff.getId()); + // for (StickIntersection inter : staffBars) { + // logger.info(inter.toString()); + // } + // + if (alignments == null) { + // Initialize the alignments + alignments = new ArrayList<>(); + system.setBarAlignments(alignments); + + for (StickIntersection crossing : staffCrossings) { + BarAlignment align = new BarAlignment(sheet, staffCount); + align.addInter(iStaff, crossing); + alignments.add(align); + } + } else { + // Do we have a bar around each abscissa? + for (StickIntersection loc : staffCrossings) { + // Find closest alignment + Double[] dists = new Double[alignments.size()]; + Integer bestIdx = null; + + for (int ia = 0; ia < alignments.size(); ia++) { + BarAlignment align = alignments.get(ia); + Double dist = align.distance(iStaff, loc); + + if (dist != null) { + dists[ia] = dist; + dist = Math.abs(dist); + + if (bestIdx != null) { + if (Math.abs(dists[bestIdx]) > dist) { + dists[bestIdx] = dist; + bestIdx = ia; + } + } else { + bestIdx = ia; + } + } + } + + if ((bestIdx != null) + && (Math.abs(dists[bestIdx]) + <= params.maxAlignmentDistance)) { + alignments.get(bestIdx) + .addInter(iStaff, loc); + } else { + // Insert a new alignment at proper index + BarAlignment align = new BarAlignment(sheet, + staffCount); + align.addInter(iStaff, loc); + + if (bestIdx == null) { + alignments.add(align); + } else { + if (dists[bestIdx] < 0) { + alignments.add(bestIdx, align); + } else { + alignments.add(bestIdx + 1, align); + } + } + } + } + } + } + + // Check the bar alignments + for (Iterator it = alignments.iterator(); it.hasNext();) { + BarAlignment align = it.next(); + + // Don't call manual alignments into question + if (align.isManual()) { + continue; + } + + // If alignment is almost empty, remove it + // otherwise, try to fill the holes + int filled = align.getFilledCount(); + + // double ratio = (double) filled / staffCount; + //// if (ratio < constants.minAlignmentRatio.getValue()) { + // // We remove this alignment and deassign its sticks + // logger.debug("{}Removing {}", sheet.getLogPrefix(), align); + // it.remove(); + // + // for (StickIntersection inter : align.getIntersections()) { + // if (inter != null) { + // inter.getStickAncestor().setShape(null); + // } + // } + // } else if (filled != staffCount) { + // // TODO: Should implement driven recognition here... + // logger.info("{}Should fill {}", sheet.getLogPrefix(), align); + // } + + // Strict: we require all staves to have a barline in this alignment + if (filled < staffCount) { + // We remove this alignment and deassign its sticks + logger.debug("{}Removing {}", sheet.getLogPrefix(), align); + it.remove(); + + for (StickIntersection inter : align.getIntersections()) { + if (inter != null) { + inter.getStickAncestor() + .setShape(null); + } + } + } + } + } + + //---------------// + // createSystems // + //---------------// + /** + * Build the frame of each system. + * + * @param tops the starting staff id for each system + * @return the sequence of system physical frames + */ + private List createSystems (Integer[] tops) + { + List newSystems = new ArrayList<>(); + Integer staffTop = null; + int systemId = 0; + SystemInfo system = null; + + for (int i = 0; i < staffManager.getStaffCount(); i++) { + StaffInfo staff = staffManager.getStaff(i); + + // System break? + if ((staffTop == null) || (staffTop < tops[i])) { + // Start of a new system + staffTop = tops[i]; + + system = new SystemInfo(++systemId, sheet, + staffManager.getRange(staff, staff)); + newSystems.add(system); + } else { + // Continuing current system + system.setStaves(staffManager.getRange(system.getFirstStaff(), + staff)); + } + } + + return newSystems; + } + + //---------------------// + // extendStaffAbscissa // + //---------------------// + /** + * Determine a not-too-bad abscissa for staff end, extending the + * abscissa beyond the bar limit if staff lines so require. + * + * @param staff the staff to process + * @param side the desired side + * @return the staff abscissa + */ + private double extendStaffAbscissa (StaffInfo staff, + HorizontalSide side) + { + // Check that staff bar, if any, is not passed by lines + BarInfo bar = staff.getBar(side); + + if (bar != null) { + Glyph drivingStick = null; + double linesX = staff.getLinesEnd(side); + + // Pick up the heaviest bar stick + SortedSet allSticks = new TreeSet<>( + Glyph.byReverseWeight); + + // Use long sticks first + for (Glyph stick : bar.getSticksAncestors()) { + if (isLong(stick)) { + allSticks.add(stick); + } + } + + // use local sticks if needed + if (allSticks.isEmpty()) { + allSticks.addAll(bar.getSticksAncestors()); + } + + if (!allSticks.isEmpty()) { + drivingStick = allSticks.first(); + + // Extend approximate limit abscissa? + Point2D inter = staff.intersection(drivingStick); + double barX = inter.getX(); + final int dir = (side == LEFT) ? 1 : (-1); + + if ((dir * (barX - linesX)) > params.maxLineExtension) { + staff.setBar(side, null); + staff.setAbscissa(side, linesX); + logger.debug("{} extended {}", side, staff); + } + } + } + + return staff.getAbscissa(side); + } + + //---------------// + // getLineEnding // + //---------------// + /** + * Report the precise point where a given line should end. + * + * @param system the system to process + * @param staff containing staff + * @param line the line at hand + * @param side the desired ending + * @return the computed ending point + * + * @throws RuntimeException DOCUMENT ME! + */ + private Point2D getLineEnding (SystemInfo system, + StaffInfo staff, + LineInfo line, + HorizontalSide side) + { + double slope = staff.getEndingSlope(side); + Object limit = system.getLimit(side); + Point2D linePt = line.getEndPoint(side); + double staffX = staff.getAbscissa(side); + double y = linePt.getY() - ((linePt.getX() - staffX) * slope); + double x; + + // Dirty programming, sorry + if (limit instanceof Glyph) { + x = ((Glyph) limit).getPositionAt(y, VERTICAL); + } else if (limit instanceof Line) { + x = ((Line) limit).xAtY(y); + } else { + throw new RuntimeException("Illegal system limit: " + limit); + } + + return new Point2D.Double(x, y); + } + + //--------------// + // mergeSystems // + //--------------// + private boolean mergeSystems () + { + List systems = sheet.getSystems(); + boolean modified = false; + + // Check connection of left barSticks across systems + for (int i = 1; i < systems.size(); i++) { + if (canConnectSystems(systems.get(i - 1), systems.get(i))) { + modified = true; + } + } + + // More touchy decisions here + // if (!modified) { + // if (constants.smartStavesConnections.getValue()) { + // return trySmartConnections(); + // } + // } else { + systemTops = null; // To force recomputation + + // } + + return modified; + } + + //-------------------// + // populateCrossings // + //-------------------// + /** + * Retrieve the sticks that intersect each staff, the results being + * kept as sequences of staff intersections in crossings structure. + */ + private void populateCrossings () + { + // Global reset + crossings.clear(); + + for (Glyph stick : filaments) { + // Skip merged sticks + if (stick.getPartOf() != null) { + continue; + } + + Point2D start = stick.getStartPoint(VERTICAL); + StaffInfo topStaff = staffManager.getStaffAt(start); + int top = topStaff.getId(); + + Point2D stop = stick.getStopPoint(VERTICAL); + StaffInfo botStaff = staffManager.getStaffAt(stop); + int bot = botStaff.getId(); + + if (logger.isDebugEnabled() || stick.isVip()) { + logger.info("Bar#{} top:{} bot:{}", stick.getId(), top, bot); + } + + for (int id = top; id <= bot; id++) { + StaffInfo staff = staffManager.getStaff(id - 1); + IntersectionSequence staffCrossings = crossings.get(staff); + + if (staffCrossings == null) { + staffCrossings = new IntersectionSequence( + StickIntersection.byAbscissa); + crossings.put(staff, staffCrossings); + } + + staffCrossings.add(new StickIntersection( + staff.intersection(stick), stick)); + } + } + } + + //---------------------// + // preciseIntersection // + //---------------------// + /** + * Compute the precise point where the vertical bar stick intersects + * the horizontal (staff) line. + * + * @param stick the vertical (bar) stick + * @param line the horizontal (staff) line + * @return the precise intersection point + */ + private Point2D preciseIntersection (Glyph stick, + LineInfo line) + { + Point2D startPoint = stick.getStartPoint(VERTICAL); + Point2D stopPoint = stick.getStopPoint(VERTICAL); + + // First, get a rough intersection + Point2D pt = LineUtil.intersection(line.getEndPoint(LEFT), + line.getEndPoint(RIGHT), + startPoint, stopPoint); + + // Second, get a precise ordinate + double y = line.yAt(pt.getX()); + + // Third, get a precise abscissa + double x; + + if (y < startPoint.getY()) { // Above stick + double invSlope = stick.getInvertedSlope(); + x = startPoint.getX() + ((y - startPoint.getY()) * invSlope); + } else if (y > stopPoint.getY()) { // Below stick + double invSlope = stick.getInvertedSlope(); + x = stopPoint.getX() + ((y - stopPoint.getY()) * invSlope); + } else { // Within stick height + x = stick.getLine().xAtY(y); + } + + return new Point2D.Double(x, y); + } + + //----------------------// + // recheckBarCandidates // + //----------------------// + /** + * Have a closer look at so-called barSticks, now that the grid of + * staves has been fully defined, to get rid of spurious barline + * sticks. + */ + private void recheckBarCandidates () + { + // Do not check barSticks already involved in side barlines + Set sideBars = getSideBarlineGlyphs(); + filaments.removeAll(sideBars); + + // Check the others, now using more strict checks + sheet.setBarsChecker(new BarsChecker(sheet, false)); + sheet.getBarsChecker().checkCandidates(filaments); + + // Purge barSticks collection + for (Iterator it = filaments.iterator(); it.hasNext();) { + Glyph stick = it.next(); + + if (!stick.isBar()) { + it.remove(); + } + } + + filaments.addAll(sideBars); + + // Purge crossings accordingly + for (IntersectionSequence seq : crossings.values()) { + for (Iterator it = seq.iterator(); + it.hasNext();) { + StickIntersection crossing = it.next(); + + if (!crossing.getStickAncestor().isBar()) { + ///logger.info("Purging " + stickPos); + it.remove(); + } + } + } + } + + //-------------------// + // refineBarsEndings // + //-------------------// + /** + * If we have reliable bar lines, refine their ending points to the + * staff lines. + * + * @param system the system to process + */ + private void refineBarsEndings (SystemInfo system) + { + if (system.getBarAlignments() == null) { + return; + } + + List staves = system.getStaves(); + + for (BarAlignment alignment : system.getBarAlignments()) { + int iStaff = -1; + + for (StickIntersection crossing : alignment.getIntersections()) { + iStaff++; + + if (crossing != null) { + StaffInfo staff = staves.get(iStaff); + Glyph stick = crossing.getStickAncestor(); + + // Left bar items have already been adjusted + BarInfo leftBar = staff.getBar(LEFT); + + if ((leftBar != null) + && leftBar.getSticksAncestors().contains(stick)) { + continue; + } + + // Perform adjustment only when on bottom staff + Point2D stop = stick.getStopPoint(VERTICAL); + StaffInfo botStaff = staffManager.getStaffAt(stop); + + if (botStaff == staff) { + Point2D start = stick.getStartPoint(VERTICAL); + StaffInfo topStaff = staffManager.getStaffAt(start); + stick.setEndingPoints( + preciseIntersection(stick, + topStaff.getFirstLine()), + preciseIntersection(stick, + botStaff.getLastLine())); + } + } + } + } + } + + //-----------------------// + // retrieveBarCandidates // + //-----------------------// + /** + * Retrieve initial barline candidates. + * + * @throws Exception + */ + private void retrieveBarCandidates () + throws Exception + { + BarsChecker barsChecker = new BarsChecker(sheet, true); // Rough + barsChecker.checkCandidates(filaments); + + // Consider only sticks with a barline shape + for (Iterator it = filaments.iterator(); it.hasNext();) { + Glyph glyph = it.next(); + if (glyph.getPartOf() != null || !glyph.isBar()) { + it.remove(); + } + } + } + + //-------------------// + // retrieveMajorBars // + //-------------------// + private void retrieveMajorBars (Collection oldGlyphs, + Collection newGlyphs) + throws Exception + { + // Build vertical filaments + if (oldGlyphs.isEmpty() && newGlyphs.isEmpty()) { + // Build filaments from scratch + buildVerticalFilaments(); + } else { + // Apply modifications to filaments + + // Removal + filaments.removeAll(oldGlyphs); + + // Addition + filaments.addAll(newGlyphs); + + // Purge non-active glyphs + for (Iterator it = filaments.iterator(); it.hasNext();) { + Glyph glyph = it.next(); + if (!glyph.isActive()) { + logger.debug("Purging non-active {}", glyph); + it.remove(); + } + } + } + + // Retrieve rough barline candidates + retrieveBarCandidates(); + + // Assign bar candidates to intersected staves + populateCrossings(); + + // Retrieve left staff bars (they define systems) and right staff bars + retrieveStaffSideBars(); + } + + //------------------// + // retrievePartTops // + //------------------// + /** + * Retrieve, for each staff, the staff that begins its containing + * part. + * + * @return the (id of) part-starting staff for each staff + */ + private Integer[] retrievePartTops () + { + staffManager.setPartTops(partTops = + new Integer[staffManager.getStaffCount()]); + + for (StaffInfo staff : staffManager.getStaves()) { + ///logger.info("Staff#" + staff.getId()); + int bot = staff.getId(); + BarInfo leftBar = staff.getBar(LEFT); + BarInfo rightBar = staff.getBar(RIGHT); + double staffLeft = staff.getAbscissa(LEFT); + IntersectionSequence staffCrossings = crossings.get(staff); + + for (StickIntersection stickCrossing : staffCrossings) { + Glyph stick = stickCrossing.getStickAncestor(); + + if (!stick.isBar()) { + continue; + } + + // Do not use left bar items (they define systems, not parts) + if ((leftBar != null) + && leftBar.getSticksAncestors().contains(stick)) { + ///logger.info("Skipping left side stick#" + stick.getId()); + continue; + } + + if (stickCrossing.x < staffLeft) { + // logger.info( + // "Too left " + stickPos.x + " vs " + staffLeft + + // " stick#" + stick.getId()); + continue; + } + + // Use right bar, if any, even if not anchored ... + // Or use a plain bar stick provided it is anchored on both sides + if ((rightBar != null && rightBar.getSticksAncestors().contains( + stick)) || (stick.getResult() == BarsChecker.BAR_PART_DEFINING)) { + Point2D start = stick.getStartPoint(VERTICAL); + StaffInfo topStaff = staffManager.getStaffAt(start); + int top = topStaff.getId(); + + for (int id = top; id <= bot; id++) { + if ((partTops[id - 1] == null) || (top < partTops[id - 1])) { + partTops[id - 1] = top; + } + } + } + } + } + + return partTops; + } + + //------------------// + // retrieveStaffBar // + //------------------// + /** + * Retrieve the (perhaps multi-bar) complete side bar, if any, of a + * given staff and record it within the staff. + * + * @param staff the given staff + * @param side proper horizontal side + */ + private void retrieveStaffBar (StaffInfo staff, + HorizontalSide side) + { + final IntersectionSequence staffCrossings = crossings.get(staff); + final int dir = (side == LEFT) ? 1 : (-1); + final double staffX = staff.getAbscissa(side); + final double xBreak = staffX + (dir * params.maxDistanceFromStaffSide); + final IntersectionSequence seq = new IntersectionSequence( + StickIntersection.byAbscissa); + BarInfo bar = null; + staff.setBar(side, null); // Reset + + // Give first importance to long (inter-staff) sticks + for (boolean takeAllSticks : new boolean[]{false, true}) { + // Browse bar sticks from outside to inside of staff + for (StickIntersection crossing + : (dir > 0) ? staffCrossings : staffCrossings.descendingSet()) { + double x = crossing.x; + + if ((dir * (xBreak - x)) < 0) { + break; // Speed up + } + + if (takeAllSticks || isLong(crossing.getStickAncestor())) { + if (seq.isEmpty()) { + seq.add(crossing); + } else { + // Perhaps a pack of bars, check total width + double width = Math.max(Math.abs(x - seq.first().x), + Math.abs(x - seq.last().x)); + + if (((side == LEFT) + && (width <= params.maxLeftBarPackWidth)) + || ((side == RIGHT) + && (width <= params.maxRightBarPackWidth))) { + seq.add(crossing); + } + } + } + } + } + + if (!seq.isEmpty()) { + seq.reduce(params.maxPosGap); // Merge sticks vertically + + double barX = seq.last().x; + + if ((dir * (barX - staffX)) <= params.maxDistanceFromStaffSide) { + bar = new BarInfo(seq.getSticks()); + staff.setBar(side, bar); + staff.setAbscissa(side, barX); + } else { + if (logger.isDebugEnabled()) { + logger.info("Staff#{} {} discarded stick#{}", + staff.getId(), side, + seq.last().getStickAncestor().getId()); + } + } + } else { + if (logger.isDebugEnabled()) { + logger.debug("Staff#{} no {} bar {}", staff.getId(), side, + Glyphs.toString(StickIntersection.sticksOf( + staffCrossings))); + } + } + + logger.debug("Staff#{} {} bar: {}", staff.getId(), side, bar); + } + + //-----------------------// + // retrieveStaffSideBars // + //-----------------------// + /** + * Retrieve staffs left bar (which define systems) and right bar. + */ + private void retrieveStaffSideBars () + { + for (HorizontalSide side : HorizontalSide.values()) { + for (StaffInfo staff : staffManager.getStaves()) { + retrieveStaffBar(staff, side); + } + } + } + + //-------------------// + // retrieveSystemBar // + //-------------------// + /** + * Merge bar sticks across staves of the same system, as long as + * they can be connected even indirectly via other sticks. + * + * @param system the system to process + * @param side the side to process + * + * @return the bar info for the system side, or null if no consistency could + * be ensured + */ + private BarInfo retrieveSystemBar (SystemInfo system, + HorizontalSide side) + { + // Horizontal sequence of bar sticks applied to the whole system side + Glyph[] seq = null; + + for (StaffInfo staff : system.getStaves()) { + BarInfo bar = staff.getBar(side); + + if (bar == null) { + return null; // We cannot ensure consistency + } + + List barSticks = bar.getSticksAncestors(); + Integer delta = null; // Delta on stick indices between 2 staves + int ib = 0; // Index on sticks + + if (seq == null) { + seq = barSticks.toArray(new Glyph[barSticks.size()]); + } else { + // Loop on bar sticks + BarLoop: + for (ib = 0; ib < barSticks.size(); ib++) { + Glyph barFil = barSticks.get(ib); + + // Check with sequence + for (int is = 0; is < seq.length; is++) { + if (seq[is].getAncestor() == barFil) { + // We have a pivot stick! + delta = is - ib; + + break BarLoop; + } + } + } + + if (delta != null) { + // Update sequence accordingly + int isMin = Math.min(0, delta); + int isBrk = Math.max(seq.length, barSticks.size() + delta); + + if ((isMin < 0) || (isBrk > seq.length)) { + // Allocate new sequence (shifted and/or enlarged) + Glyph[] newSeq = new Glyph[isBrk - isMin]; + System.arraycopy(seq, 0, newSeq, -isMin, seq.length); + seq = newSeq; + } + + for (ib = 0; ib < barSticks.size(); ib++) { + Glyph barStick = barSticks.get(ib); + int is = (ib + delta) - isMin; + Glyph seqStick = seq[is]; + + if (seqStick != null) { + seqStick = seqStick.getAncestor(); + + if (seqStick != barStick) { + logger.debug("Including F{} to F{}", + barStick.getId(), seqStick.getId()); + seqStick.stealSections(barStick); + } + } else { + seq[is] = barStick; + } + } + } else { + return null; + } + } + } + + BarInfo systemBar = new BarInfo(seq); + system.setBar(side, systemBar); + + return systemBar; + } + + //--------------------// + // retrieveSystemTops // + //--------------------// + /** + * Retrieve for each staff the staff that begins its containing + * system. + * + * @return the (id of) system-starting staff for each staff + */ + private Integer[] retrieveSystemTops () + { + staffManager.setSystemTops(systemTops = + new Integer[staffManager.getStaffCount()]); + + for (StaffInfo staff : staffManager.getStaves()) { + int bot = staff.getId(); + BarInfo bar = staff.getBar(LEFT); + + if (bar != null) { + // We have a starting bar line + for (Glyph stick : bar.getSticksAncestors()) { + Point2D start = stick.getStartPoint(VERTICAL); + StaffInfo topStaff = staffManager.getStaffAt(start); + int top = topStaff.getId(); + + for (int id = top; id <= bot; id++) { + if ((systemTops[id - 1] == null) + || (top < systemTops[id - 1])) { + systemTops[id - 1] = top; + } + } + } + } else { + // We have no starting bar line, so staff = part = system + systemTops[bot - 1] = bot; + } + } + + return systemTops; + } + + //--------------------// + // tryRangeConnection // + //--------------------// + /** + * Try to connect all systems in the provided range. + * + * @param range sublist of systems + * + * @return true if OK + */ + private boolean tryRangeConnection (List range) + { + final SystemInfo firstSystem = range.get(0); + final StaffInfo firstStaff = firstSystem.getFirstStaff(); + final int topId = firstStaff.getId(); + int idx = staffManager.getStaves() + .indexOf(firstStaff); + + for (SystemInfo system : range) { + for (StaffInfo staff : system.getStaves()) { + systemTops[idx++] = topId; + } + } + + logger.info("Staves connection from {} to {}", topId, + range.get(range.size() - 1).getLastStaff().getId()); + + return true; + } + + //~ Inner Classes ---------------------------------------------------------- + // //---------------------// + // // trySmartConnections // + // //---------------------// + // /** + // * Method to try system connections not based on local bar connections + // * @return true if connection decided + // */ + // private boolean trySmartConnections () + // { + // // Valuable hints: + // // - Significant differences in systems lengths + // // - System w/o left bar, while others have some + // // - Bar that significantly departs from a staff + // + // // Systems lengths + // List lengths = new ArrayList(systems.size()); + // + // // Extrema + // int smallestLength = Integer.MAX_VALUE; + // int largestLength = Integer.MIN_VALUE; + // + // for (SystemInfo system : systems) { + // int length = system.getStaves() + // .size(); + // lengths.add(length); + // smallestLength = Math.min(smallestLength, length); + // largestLength = Math.max(largestLength, length); + // } + // + // // If all systems are equal in length, there is nothing to try + // if (smallestLength == largestLength) { + // return false; + // } + // + // if ((2 * largestLength) == staffManager.getStaffCount()) { + // // Check that we can add up to largest size + // if (lengths.get(0) == largestLength) { + // return tryRangeConnection(systems.subList(1, lengths.size())); + // } else if (lengths.get(lengths.size() - 1) == largestLength) { + // return tryRangeConnection( + // systems.subList(0, lengths.size() - 1)); + // } + // } else if ((3 * largestLength) == staffManager.getStaffCount()) { + // // Check that we can add up to largest size + // // TBD + // } + // + // return false; + // } + //-----------// + // Constants // + //-----------// + /** + * TODO: This collection of parameters is way too long! It should be + * carefully reduced!!! + */ + private static final class Constants + extends ConstantSet + { + //~ Instance fields ---------------------------------------------------- + + Constant.Ratio maxLengthRatio = new Constant.Ratio( + 1.4, + "Maximum ratio in length for a run to be combined with an existing section"); + + // Constants specified WRT mean interline + // -------------------------------------- + Scale.Fraction maxSectionThickness = new Scale.Fraction( + 0.8, + "Maximum horizontal section thickness WRT interline"); + + Scale.Fraction maxFilamentThickness = new Scale.Fraction( + 0.8, + "Maximum horizontal filament thickness WRT interline"); + + Scale.Fraction maxOverlapDeltaPos = new Scale.Fraction( + 0.4, + "Maximum delta position between two overlapping filaments"); + + Scale.Fraction maxCoordGap = new Scale.Fraction( + 0.5, + "Maximum delta coordinate for a gap between filaments"); + + Scale.Fraction maxPosGap = new Scale.Fraction( + 0.2, + "Maximum delta abscissa for a gap between filaments"); + + Scale.Fraction maxSpace = new Scale.Fraction( + 0.1, + "Maximum space between overlapping bar filaments"); + + Scale.Fraction maxBarCoordGap = new Scale.Fraction( + 2, + "Maximum delta coordinate for a vertical gap between bars"); + + Scale.Fraction maxBarPosGap = new Scale.Fraction( + 0.3, + "Maximum delta position for a vertical gap between bars"); + + Scale.Fraction minRunLength = new Scale.Fraction( + 1.5, + "Minimum length for a vertical run to be considered"); + + Scale.Fraction minLongLength = new Scale.Fraction( + 8, + "Minimum length for a long vertical bar"); + + Scale.Fraction maxDistanceFromStaffSide = new Scale.Fraction( + 2, + "Max abscissa delta when looking for left or right side bars"); + + Scale.Fraction maxLeftBarPackWidth = new Scale.Fraction( + 1.5, + "Max width of a pack of vertical barlines"); + + Scale.Fraction maxRightBarPackWidth = new Scale.Fraction( + 1, + "Max width of a pack of vertical barlines"); + + Scale.Fraction maxSideDx = new Scale.Fraction( + .5, + "Max difference on theoretical bar abscissa"); + + Scale.Fraction maxLineExtension = new Scale.Fraction( + .5, + "Max extension of line beyond staff bar"); + + Scale.Fraction minBarChunkHeight = new Scale.Fraction( + 1, + "Min height of a bar chunk past system boundaries"); + + Scale.Fraction maxAlignmentDistance = new Scale.Fraction( + 0.5, + "Max horizontal shift between aligned bars"); + + Constant.Ratio minAlignmentRatio = new Constant.Ratio( + 0.5, + "Minimum percentage of mapped staves in a bar alignment "); + + // Constants for display + // + Constant.Boolean showVerticalLines = new Constant.Boolean( + false, + "Should we display the vertical lines?"); + + Constant.Boolean showTangents = new Constant.Boolean( + false, + "Should we show filament ending tangents?"); + + Constant.Double splineThickness = new Constant.Double( + "thickness", 0.5, + "Stroke thickness to draw filaments curves"); + + Constant.Boolean smartStavesConnections = new Constant.Boolean( + true, + "(beta) Should we try smart staves connections into systems?"); + + // Constants for debugging + // + Constant.String verticalVipSections = new Constant.String( + "", + "(Debug) Comma-separated list of VIP sections"); + + } + + //------------// + // Parameters // + //------------// + /** + * Class {@code Parameters} gathers all constants related to vertical + * frames. + */ + private static class Parameters + { + //~ Static fields/initializers ----------------------------------------- + + /** Usual logger utility. */ + private static final Logger logger = LoggerFactory.getLogger(Parameters.class); + + //~ Instance fields ---------------------------------------------------- + /** Maximum delta abscissa for a gap between filaments. */ + final int maxPosGap; + + /** Minimum run length for vertical lag. */ + final int minRunLength; + + /** Used for section junction policy. */ + final double maxLengthRatio; + + /** Minimum for long vertical stick bars. */ + final int minLongLength; + + /** Maximum distance between a bar and the staff side. */ + final int maxDistanceFromStaffSide; + + /** Maximum width for a pack of bars on left side. */ + final int maxLeftBarPackWidth; + + /** Maximum width for a pack of bars on right side. */ + final int maxRightBarPackWidth; + + /** Max difference on theoretical bar abscissa. */ + final int maxSideDx; + + /** Max extension of line beyond staff bar. */ + final int maxLineExtension; + + /** Min height to detect a bar going past a staff. */ + final int minBarChunkHeight; + + /** Maximum abscissa shift for bar alignments. */ + final double maxAlignmentDistance; + + /** Maximum delta position for a vertical gap between bars. */ + final int maxBarPosGap; + + /** Maximum delta coordinate for a vertical gap between bars. */ + final int maxBarCoordGap; + + // Debug + final List vipSections; + + //~ Constructors ------------------------------------------------------- + /** + * Creates a new Parameters object. + * + * @param scale the scaling factor + */ + public Parameters (Scale scale) + { + maxPosGap = scale.toPixels(constants.maxPosGap); + minRunLength = scale.toPixels(constants.minRunLength); + maxLengthRatio = constants.maxLengthRatio.getValue(); + minLongLength = scale.toPixels(constants.minLongLength); + maxDistanceFromStaffSide = scale.toPixels( + constants.maxDistanceFromStaffSide); + maxLeftBarPackWidth = scale.toPixels(constants.maxLeftBarPackWidth); + maxRightBarPackWidth = scale. + toPixels(constants.maxRightBarPackWidth); + maxSideDx = scale.toPixels(constants.maxSideDx); + maxLineExtension = scale.toPixels(constants.maxLineExtension); + minBarChunkHeight = scale.toPixels(constants.minBarChunkHeight); + maxAlignmentDistance = scale. + toPixels(constants.maxAlignmentDistance); + maxBarPosGap = scale.toPixels(constants.maxBarPosGap); + maxBarCoordGap = scale.toPixels(constants.maxBarCoordGap); + + // VIPs + vipSections = VipUtil.decodeIds( + constants.verticalVipSections.getValue()); + + if (logger.isDebugEnabled()) { + Main.dumping.dump(this); + } + + if (!vipSections.isEmpty()) { + logger.info("Vertical VIP sections: {}", vipSections); + } + } + } +} diff --git a/src/main/omr/grid/ClustersRetriever.java b/src/main/omr/grid/ClustersRetriever.java new file mode 100644 index 0000000..9a832da --- /dev/null +++ b/src/main/omr/grid/ClustersRetriever.java @@ -0,0 +1,1197 @@ +//----------------------------------------------------------------------------// +// // +// C l u s t e r s R e t r i e v e r // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.grid; + +import omr.Main; + +import omr.constant.Constant; +import omr.constant.ConstantSet; + +import omr.glyph.Glyphs; +import omr.glyph.Shape; +import omr.glyph.facets.Glyph; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import omr.math.Histogram; + +import omr.run.Orientation; +import static omr.run.Orientation.*; + +import omr.sheet.Scale; +import omr.sheet.Sheet; +import omr.sheet.Skew; + +import omr.util.Wrapper; + +import java.awt.Color; +import java.awt.Graphics2D; +import java.awt.Point; +import java.awt.Rectangle; +import java.awt.geom.Line2D; +import java.awt.geom.Point2D; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.TreeMap; + +/** + * Class {@code ClustersRetriever} performs vertical samplings of the + * horizontal filaments in order to detect regular patterns of a + * preferred interline value and aggregate the filaments into clusters + * of lines. + * + * @author Hervé Bitteur + */ +public class ClustersRetriever +{ + //~ Static fields/initializers --------------------------------------------- + + /** Specific application parameters */ + private static final Constants constants = new Constants(); + + /** Usual logger utility */ + private static final Logger logger = LoggerFactory.getLogger( + ClustersRetriever.class); + + /** + * For comparing Filament instances on their starting point + */ + private static final Comparator startComparator = new Comparator() + { + @Override + public int compare (Filament f1, + Filament f2) + { + // Sort on start + return Double.compare( + f1.getStartPoint(HORIZONTAL).getX(), + f2.getStartPoint(HORIZONTAL).getX()); + } + }; + + /** + * For comparing Filament instances on their stopping point + */ + private static final Comparator stopComparator = new Comparator() + { + @Override + public int compare (Filament f1, + Filament f2) + { + // Sort on stop + return Double.compare( + f1.getStopPoint(HORIZONTAL).getX(), + f2.getStopPoint(HORIZONTAL).getX()); + } + }; + + //~ Instance fields -------------------------------------------------------- + /** Comparator on cluster ordinate */ + public Comparator ordinateComparator = new Comparator() + { + @Override + public int compare (LineCluster c1, + LineCluster c2) + { + double o1 = ordinateOf(c1); + double o2 = ordinateOf(c2); + + if (o1 < o2) { + return -1; + } + + if (o1 > o2) { + return +1; + } + + return 0; + } + }; + + /** Related sheet */ + private final Sheet sheet; + + /** Related scale */ + private final Scale scale; + + /** Desired interline */ + private final int interline; + + /** Scale-dependent constants */ + private final Parameters params; + + /** Picture width to sample for combs */ + private final int pictureWidth; + + /** Long filaments to process */ + private final List filaments; + + /** Filaments discarded */ + private final List discardedFilaments = new ArrayList<>(); + + /** Skew of the sheet */ + private final Skew skew; + + /** A map (colIndex -> vertical list of samples), sorted on colIndex */ + private Map> colCombs; + + /** Color used for comb display */ + private final Color combColor; + + /** + * The popular size of combs detected for the specified interline + * (typically: 4, 5 or 6) + */ + private int popSize; + + /** X values per column index */ + private int[] colX; + + /** Collection of clusters */ + private final List clusters = new ArrayList<>(); + + //~ Constructors ----------------------------------------------------------- + //-------------------// + // ClustersRetriever // + //-------------------// + /** + * Creates a new ClustersRetriever object, for a given staff + * interline. + * + * @param sheet the sheet to process + * @param filaments the current collection of filaments + * @param interline the precise interline to be processed + * @param combColor color to be used for combs display + */ + public ClustersRetriever (Sheet sheet, + List filaments, + int interline, + Color combColor) + { + this.sheet = sheet; + this.filaments = filaments; + this.interline = interline; + this.combColor = combColor; + + skew = sheet.getSkew(); + pictureWidth = sheet.getWidth(); + scale = sheet.getScale(); + colCombs = new TreeMap<>(); + + params = new Parameters(scale); + } + + //~ Methods ---------------------------------------------------------------- + //-----------// + // buildInfo // + //-----------// + public List buildInfo () + { + // Retrieve all vertical combs gathering filaments + retrieveCombs(); + + // Remember the most popular comb length + retrievePopularSize(); + + // Check relevance + if ((popSize < 4) || (popSize > 6)) { + logger.info("{}Giving up spurious line comb size: {}", + sheet.getLogPrefix(), popSize); + + return discardedFilaments; + } + + // Interconnect filaments via the network of combs + followCombsNetwork(); + + // Retrieve clusters + retrieveClusters(); + + logger.info( + "{}Retrieved line clusters: {} of size: {} with interline: {}", + sheet.getLogPrefix(), clusters.size(), popSize, interline); + + return discardedFilaments; + } + + //-------------// + // getClusters // + //-------------// + /** + * Report the sequence of clusters detected by this retriever using + * its provided interline value. + * + * @return the sequence of interline-based clusters + */ + public List getClusters () + { + return clusters; + } + + //--------------// + // getInterline // + //--------------// + /** + * Report the value of the interline this retriever is based upon + * + * @return the interline value + */ + public int getInterline () + { + return interline; + } + + //--------------------// + // getStafflineGlyphs // + //--------------------// + /** + * Report the glyphs that are part of actual cluster lines + * + * @return the collection of glyphs actually used for retained cluster + */ + Collection getStafflineGlyphs () + { + List glyphs = new ArrayList<>(); + + for (LineCluster cluster : clusters) { + for (FilamentLine line : cluster.getLines()) { + glyphs.add(line.fil); + } + } + + return glyphs; + } + + //-------------// + // renderItems // + //-------------// + /** + * Render the vertical combs of filaments + * + * @param g graphics context + */ + void renderItems (Graphics2D g) + { + Color oldColor = g.getColor(); + g.setColor(combColor); + + for (Entry> entry : colCombs.entrySet()) { + int col = entry.getKey(); + int x = colX[col]; + + for (FilamentComb comb : entry.getValue()) { + g.draw( + new Line2D.Double( + x, + comb.getY(0), + x, + comb.getY(comb.getCount() - 1))); + } + } + + g.setColor(oldColor); + } + + //-----------// + // bestMatch // + //-----------// + /** + * Find the best match between provided sequences. + * (which may contain null values when related data is not available) + * + * @param one first sequence + * @param two second sequence + * @param bestDelta output: best delta between the two sequences + * @return the best distance found + */ + private double bestMatch (Double[] one, + Double[] two, + Wrapper bestDelta) + { + final int deltaMax = one.length - 1; + final int deltaMin = -deltaMax; + + double bestDist = Double.MAX_VALUE; + bestDelta.value = null; + + for (int delta = deltaMin; delta <= deltaMax; delta++) { + int distSum = 0; + int count = 0; + + for (int oneIdx = 0; oneIdx < one.length; oneIdx++) { + int twoIdx = oneIdx + delta; + + if ((twoIdx >= 0) && (twoIdx < two.length)) { + Double oneVal = one[oneIdx]; + Double twoVal = two[twoIdx]; + + if ((oneVal != null) && (twoVal != null)) { + count++; + distSum += Math.abs(twoVal - oneVal); + } + } + } + + if (count > 0) { + double dist = (double) distSum / count; + + if (dist < bestDist) { + bestDist = dist; + bestDelta.value = delta; + } + } + } + + return bestDist; + } + + //----------// + // canMerge // + //----------// + /** + * Check for merge possibility between two clusters + * + * @param one first cluster + * @param two second cluster + * @param deltaPos output: the delta in positions between these clusters + * if the test has succeeded + * @return true if successful + */ + private boolean canMerge (LineCluster one, + LineCluster two, + Wrapper deltaPos) + { + final Rectangle oneBox = one.getBounds(); + final Rectangle twoBox = two.getBounds(); + + final int oneLeft = oneBox.x; + final int oneRight = (oneBox.x + oneBox.width) - 1; + final int twoLeft = twoBox.x; + final int twoRight = (twoBox.x + twoBox.width) - 1; + + final int minRight = Math.min(oneRight, twoRight); + final int maxLeft = Math.max(oneLeft, twoLeft); + final int gap = maxLeft - minRight; + double dist; + + logger.debug("gap:{}", gap); + + if (gap <= 0) { + // Overlap: use middle of common part + final int xMid = (maxLeft + minRight) / 2; + final double slope = sheet.getSkew().getSlope(); + dist = bestMatch( + ordinatesOf( + one.getPointsAt(xMid, params.maxExpandDx, interline, slope)), + ordinatesOf( + two.getPointsAt(xMid, params.maxExpandDx, interline, slope)), + deltaPos); + } else if (gap > params.maxMergeDx) { + logger.debug("Gap too wide between {} & {}", one, two); + + return false; + } else { + // True gap: use proper edges + if (oneLeft < twoLeft) { // Case one --- two + dist = bestMatch( + ordinatesOf(one.getStops()), + ordinatesOf(two.getStarts()), + deltaPos); + } else { // Case two --- one + dist = bestMatch( + ordinatesOf(one.getStarts()), + ordinatesOf(two.getStops()), + deltaPos); + } + } + + // Check best distance + logger.debug("canMerge dist: {} one:{} two:{}", dist, one, two); + + return dist <= params.maxMergeDy; + } + + //-------------------------// + // computeAcceptableLength // + //-------------------------// + private double computeAcceptableLength () + { + // Determine minimum true length for valid clusters + List lengths = new ArrayList<>(); + + for (LineCluster cluster : clusters) { + lengths.add(cluster.getTrueLength()); + } + + Collections.sort(lengths); + + int medianLength = lengths.get(lengths.size() / 2); + double minLength = medianLength * constants.minClusterLengthRatio. + getValue(); + + logger.debug("medianLength: {} minLength: {}", medianLength, minLength); + + return minLength; + } + + //------------------// + // connectAncestors // + //------------------// + private void connectAncestors (LineFilament one, + LineFilament two) + { + LineFilament oneAnc = (LineFilament) one.getAncestor(); + LineFilament twoAnc = (LineFilament) two.getAncestor(); + + if (oneAnc != twoAnc) { + if (oneAnc.getLength(Orientation.HORIZONTAL) >= twoAnc.getLength( + Orientation.HORIZONTAL)) { + ///logger.info("Inclusion " + twoAnc + " into " + oneAnc); + oneAnc.include(twoAnc); + oneAnc.getCombs().putAll(twoAnc.getCombs()); + } else { + ///logger.info("Inclusion " + oneAnc + " into " + twoAnc); + twoAnc.include(oneAnc); + twoAnc.getCombs().putAll(oneAnc.getCombs()); + } + } + } + + //----------------// + // createClusters // + //----------------// + private void createClusters () + { + Collections.sort( + filaments, + Glyphs.getReverseLengthComparator(Orientation.HORIZONTAL)); + + for (LineFilament fil : filaments) { + fil = (LineFilament) fil.getAncestor(); + + if ((fil.getCluster() == null) && !fil.getCombs().isEmpty()) { + LineCluster cluster = new LineCluster(interline, fil); + clusters.add(cluster); + } + } + + removeMergedClusters(); + } + + //----------------------------// + // destroyNonStandardClusters // + //----------------------------// + private void destroyNonStandardClusters () + { + for (Iterator it = clusters.iterator(); it.hasNext();) { + LineCluster cluster = it.next(); + + if (cluster.getSize() != popSize) { + logger.debug("Destroying non standard {}", cluster); + + cluster.destroy(); + it.remove(); + } + } + } + + //------------------------------// + // discardNonClusteredFilaments // + //------------------------------// + private void discardNonClusteredFilaments () + { + for (Iterator it = filaments.iterator(); it.hasNext();) { + LineFilament fil = it.next(); + + if (fil.getCluster() == null) { + it.remove(); + discardedFilaments.add(fil); + } else { + fil.setShape(Shape.STAFF_LINE); + } + } + } + + //--------------// + // dumpClusters // + //--------------// + private void dumpClusters () + { + for (LineCluster cluster : clusters) { + logger.info("{} {}", cluster.getCenter(), cluster.toString()); + } + } + + //---------------// + // expandCluster // + //---------------// + /** + * Try to expand the provided cluster with filaments taken out of + * the provided sorted collection of isolated filaments + * + * @param cluster the cluster to work on + * @param fils the (properly sorted) collection of filaments + */ + private void expandCluster (LineCluster cluster, + List fils) + { + final double slope = sheet.getSkew().getSlope(); + Rectangle clusterBox = null; + + for (LineFilament fil : fils) { + fil = (LineFilament) fil.getAncestor(); + + if (fil.getCluster() != null) { + continue; + } + + // For VIP debugging + final boolean areVips = cluster.isVip() && fil.isVip(); + String vips = null; + + if (areVips) { + vips = "F" + fil.getId() + "&C" + cluster.getId() + ": "; // BP here! + } + + if (clusterBox == null) { + clusterBox = cluster.getBounds(); + clusterBox.grow(params.clusterXMargin, params.clusterYMargin); + } + + Rectangle filBox = fil.getBounds(); + Point middle = new Point(); + middle.x = filBox.x + (filBox.width / 2); + middle.y = (int) Math.rint(fil.getPositionAt(middle.x, HORIZONTAL)); + + if (clusterBox.contains(middle)) { + // Check if this filament matches a cluster line + List points = cluster.getPointsAt( + middle.x, + params.maxExpandDx, + interline, + slope); + + for (Point2D point : points) { + // Check vertical distance, if point is available + if (point == null) { + continue; + } + + double dy = Math.abs(middle.y - point.getY()); + + if (dy <= params.maxExpandDy) { + int index = points.indexOf(point); + + if (cluster.includeFilamentByIndex(fil, index)) { + if (logger.isDebugEnabled() + || fil.isVip() + || cluster.isVip()) { + logger.info( + "Aggregated F{} to C{} at index {}", + fil.getId(), cluster.getId(), index); + + if (fil.isVip()) { + cluster.setVip(); + } + } + + clusterBox = null; // Invalidate cluster box + + break; + } + } else { + if (areVips) { + logger.info("{}dy={} vs {}", + vips, dy, params.maxExpandDy); + } + } + } + } else { + if (areVips) { + logger.info("{}No box intersection", vips); + } + } + } + } + + //----------------// + // expandClusters // + //----------------// + /** + * Aggregate non-clustered filaments to close clusters when + * appropriate. + */ + private void expandClusters () + { + List startFils = new ArrayList<>(filaments); + Collections.sort(startFils, startComparator); + + List stopFils = new ArrayList<>(startFils); + Collections.sort(stopFils, stopComparator); + + // Browse clusters, starting with the longest ones + Collections.sort(clusters, LineCluster.reverseLengthComparator); + + for (LineCluster cluster : clusters) { + logger.debug("Expanding {}", cluster); + + // Expanding on left side + expandCluster(cluster, stopFils); + // Expanding on right side + expandCluster(cluster, startFils); + } + } + + //--------------------// + // followCombsNetwork // + //--------------------// + /** + * Use the network of combs and filaments to interconnect filaments + * via common combs. + */ + private void followCombsNetwork () + { + logger.debug("Following combs network"); + + for (LineFilament fil : filaments) { + Map combs = fil.getCombs(); + + // Sequence of lines around the filament, indexed by relative pos + Map lines = new TreeMap<>(); + + // Loop on all combs this filament is involved in + for (FilamentComb comb : combs.values()) { + int posPivot = comb.getIndex(fil); + + for (int pos = 0; pos < comb.getCount(); pos++) { + int line = pos - posPivot; + + if (line != 0) { + LineFilament f = lines.get(line); + + if (f != null) { + connectAncestors(f, comb.getFilament(pos)); + } else { + lines.put(line, comb.getFilament(pos)); + } + } + } + } + } + + removeMergedFilaments(); + } + + //-------------------// + // mergeClusterPairs // + //-------------------// + /** + * Merge clusters horizontally or destroy short clusters. + */ + private void mergeClusterPairs () + { + // Sort clusters according to their ordinate in page + Collections.sort(clusters, ordinateComparator); + + double minLength = computeAcceptableLength(); + WholeLoop: + for (int idx = 0; idx < clusters.size();) { + LineCluster cluster = clusters.get(idx); + Point2D dskCenter = skew.deskewed(cluster.getCenter()); + double yMax = dskCenter.getY() + params.maxMergeCenterDy; + + for (LineCluster cl : clusters.subList(idx + 1, clusters.size())) { + // Check dy + if (skew.deskewed(cl.getCenter()).getY() > yMax) { + break; + } + + // Merge + logger.info("Pairing clusters C{} & C{}", + cluster.getId(), cl.getId()); + cluster.mergeWith(cl, 0); + clusters.remove(cl); + + continue WholeLoop; // Recheck at same index + } + + // Short isolated? + if (cluster.getTrueLength() < minLength) { + logger.info("Destroying spurious {}", cluster); + clusters.remove(cluster); + } else { + idx++; // Move forward + } + } + + removeMergedFilaments(); + } + + //---------------// + // mergeClusters // + //---------------// + /** + * Merge compatible clusters as much as possible. + */ + private void mergeClusters () + { + // Sort clusters according to their ordinate in page + Collections.sort(clusters, ordinateComparator); + + for (LineCluster current : clusters) { + LineCluster candidate = current; + + // Keep on working while we do have a candidate to check for merge + CandidateLoop: + while (true) { + Wrapper deltaPos = new Wrapper<>(); + Rectangle candidateBox = candidate.getBounds(); + candidateBox.grow(params.clusterXMargin, params.clusterYMargin); + + // Check the candidate vs all clusters until current excluded + for (LineCluster head : clusters) { + if (head == current) { + break CandidateLoop; // Actual end of sub list + } + + if ((head == candidate) || (head.getParent() != null)) { + continue; + } + + // Check rough proximity + Rectangle headBox = head.getBounds(); + + if (headBox.intersects(candidateBox)) { + // Try a merge + if (canMerge(head, candidate, deltaPos)) { + logger.debug("Merging {} with {} delta:{}", + candidate, head, deltaPos.value); + + // Do the merge + candidate.mergeWith(head, deltaPos.value); + + break; + } + } + } + } + } + + removeMergedClusters(); + removeMergedFilaments(); + } + + //------------// + // ordinateOf // + //------------// + /** + * Report the orthogonal distance of the provided point + * to the sheet top edge tilted with global slope. + */ + private Double ordinateOf (Point2D point) + { + if (point != null) { + return sheet.getSkew().deskewed(point).getY(); + } else { + return null; + } + } + + //------------// + // ordinateOf // + //------------// + /** + * Report the orthogonal distance of the cluster center + * to the sheet top edge tilted with global slope. + */ + private double ordinateOf (LineCluster cluster) + { + return ordinateOf(cluster.getCenter()); + } + + //-------------// + // ordinatesOf // + //-------------// + private Double[] ordinatesOf (Collection points) + { + Double[] ys = new Double[points.size()]; + int index = 0; + + for (Point2D p : points) { + ys[index++] = ordinateOf(p); + } + + return ys; + } + + //----------------------// + // removeMergedClusters // + //----------------------// + private void removeMergedClusters () + { + for (Iterator it = clusters.iterator(); it.hasNext();) { + LineCluster cluster = it.next(); + + if (cluster.getParent() != null) { + it.remove(); + } + } + } + + //-----------------------// + // removeMergedFilaments // + //-----------------------// + private void removeMergedFilaments () + { + for (Iterator it = filaments.iterator(); it.hasNext();) { + LineFilament fil = it.next(); + + if (fil.getPartOf() != null) { + it.remove(); + } + } + } + + //------------------// + // retrieveClusters // + //------------------// + /** + * Connect filaments via the combs they are involved in, + * and come up with clusters of lines. + */ + private void retrieveClusters () + { + // Create clusters recursively out of filements + createClusters(); + + // Aggregate filaments left over when possible (first) + expandClusters(); + + // Merge clusters + mergeClusters(); + + // Trim clusters with too many lines + trimClusters(); + + // Discard non standard clusters + destroyNonStandardClusters(); + + // Merge clusters horizontally + mergeClusterPairs(); + + // Aggregate filaments left over when possible (second) + expandClusters(); + + // Discard non-clustered filaments + discardNonClusteredFilaments(); + + removeMergedFilaments(); + + // Debug + if (logger.isDebugEnabled()) { + dumpClusters(); + } + } + + //---------------// + // retrieveCombs // + //---------------// + /** + * Detect regular patterns of (staff) lines. + * Use vertical sampling on regularly-spaced abscissae + */ + private void retrieveCombs () + { + // /** Minimum acceptable delta y */ + // int dMin = (int) Math.floor( + // interline * (1 - constants.maxJitter.getValue())); + // + // /** Maximum acceptable delta y */ + // int dMax = (int) Math.ceil( + // interline * (1 + constants.maxJitter.getValue())); + /** Minimum acceptable delta y */ + int dMin = (int) Math.floor(scale.getMinInterline()); + + /** Maximum acceptable delta y */ + int dMax = (int) Math.ceil(scale.getMaxInterline()); + + /** Number of vertical samples to collect */ + int sampleCount = -1 + + (int) Math.rint( + (double) pictureWidth / params.samplingDx); + + /** Exact columns abscissae */ + colX = new int[sampleCount + 1]; + + /** Precise x interval */ + double samplingDx = (double) pictureWidth / (sampleCount + 1); + + for (int col = 1; col <= sampleCount; col++) { + final List colList = new ArrayList<>(); + colCombs.put(col, colList); + + final int x = (int) Math.rint(samplingDx * col); + colX[col] = x; + + // Retrieve Filaments with ordinate at x, sorted by increasing y + List filys = retrieveFilamentsAtX(x); + + // Second, check y deltas to detect combs + FilamentComb comb = null; + FilY prevFily = null; + + for (FilY fily : filys) { + if (prevFily != null) { + int dy = (int) Math.rint(fily.y - prevFily.y); + + if ((dy >= dMin) && (dy <= dMax)) { + if (comb == null) { + // Start of a new comb + comb = new FilamentComb(col); + colList.add(comb); + comb.append(prevFily.filament, prevFily.y); + + if (prevFily.filament.isVip()) { + logger.info("Created {} with {}", + comb, prevFily.filament); + } + } + + // Extend comb + comb.append(fily.filament, fily.y); + + if (fily.filament.isVip()) { + logger.info("Appended {} to {}", + fily.filament, comb); + } + } else { + // No comb active + comb = null; + } + } + + prevFily = fily; + } + } + } + + //----------------------// + // retrieveFilamentsAtX // + //----------------------// + /** + * For a given abscissa, retrieve the filaments that are intersected + * by vertical x, and sort them according to their ordinate at x. + * + * @param x the desired abscissa + * @return the sorted list of structures (Fil + Y), perhaps empty + */ + private List retrieveFilamentsAtX (double x) + { + List list = new ArrayList<>(); + + for (LineFilament fil : filaments) { + if ((x >= fil.getStartPoint(HORIZONTAL).getX()) + && (x <= fil.getStopPoint(HORIZONTAL).getX())) { + list.add(new FilY(fil, fil.getPositionAt(x, HORIZONTAL))); + } + } + + Collections.sort(list); + + return list; + } + + //---------------------// + // retrievePopularSize // + //---------------------// + /** + * Retrieve the most popular size (line count) among all combs. + */ + private void retrievePopularSize () + { + // Build histogram of combs lengths + Histogram histo = new Histogram<>(); + + for (List list : colCombs.values()) { + for (FilamentComb comb : list) { + histo.increaseCount(comb.getCount(), comb.getCount()); + } + } + + // Use the most popular length + // Should be 4 for bass tab, 5 for standard notation, 6 for guitar tab + popSize = histo.getMaxBucket(); + + logger.debug("{}Popular line comb: {} histo:{}", + sheet.getLogPrefix(), popSize, histo.dataString()); + } + + //--------------// + // trimClusters // + //--------------// + private void trimClusters () + { + Collections.sort(clusters, ordinateComparator); + + // Trim clusters with too many lines + for (Iterator it = clusters.iterator(); it.hasNext();) { + LineCluster cluster = it.next(); + cluster.trim(popSize); + } + } + + //~ Inner Classes ---------------------------------------------------------- + //-----------// + // Constants // + //-----------// + private static final class Constants + extends ConstantSet + { + //~ Instance fields ---------------------------------------------------- + + Scale.Fraction samplingDx = new Scale.Fraction( + 1, + "Typical delta X between two vertical samplings"); + + Scale.Fraction maxExpandDx = new Scale.Fraction( + 2, + "Maximum dx to aggregate a filament to a cluster"); + + Scale.Fraction maxExpandDy = new Scale.Fraction( + 0.175, + "Maximum dy to aggregate a filament to a cluster"); + + Scale.Fraction maxMergeDx = new Scale.Fraction( + 10, + "Maximum dx to merge two clusters"); + + Scale.Fraction maxMergeDy = new Scale.Fraction( + 0.4, + "Maximum dy to merge two clusters"); + + Scale.Fraction maxMergeCenterDy = new Scale.Fraction( + 1.0, + "Maximum center dy to merge two clusters"); + + Scale.Fraction clusterXMargin = new Scale.Fraction( + 4, + "Rough margin around cluster abscissa"); + + Scale.Fraction clusterYMargin = new Scale.Fraction( + 2, + "Rough margin around cluster ordinate"); + + Constant.Ratio maxJitter = new Constant.Ratio( + 0.1, + "Maximum gap from standard comb dy"); + + Constant.Ratio minClusterLengthRatio = new Constant.Ratio( + 0.3, + "Minimum cluster length (as ratio of median length)"); + + } + + //------// + // FilY // + //------// + /** + * Class meant to define an ordering relationship between filaments, + * knowing their ordinate at a common abscissa value. + */ + private static class FilY + implements Comparable + { + //~ Instance fields ---------------------------------------------------- + + final LineFilament filament; + + final double y; + + //~ Constructors ------------------------------------------------------- + public FilY (LineFilament filament, + double y) + { + this.filament = filament; + this.y = y; + } + + //~ Methods ------------------------------------------------------------ + @Override + public int compareTo (FilY that) + { + return Double.compare(this.y, that.y); + } + + @Override + public String toString () + { + return "{F" + filament.getId() + " y:" + y + "}"; + } + } + + //------------// + // Parameters // + //------------// + /** + * Class {@code Parameters} gathers all constants related to + * horizontal frames. + */ + private static class Parameters + { + //~ Instance fields ---------------------------------------------------- + + final int samplingDx; + + final int maxExpandDx; + + final int maxExpandDy; + + final int maxMergeDx; + + final int maxMergeDy; + + final int maxMergeCenterDy; + + final int clusterXMargin; + + final int clusterYMargin; + + //~ Constructors ------------------------------------------------------- + /** + * Creates a new Parameters object. + * + * @param scale the scaling factor + */ + public Parameters (Scale scale) + { + samplingDx = scale.toPixels(constants.samplingDx); + maxExpandDx = scale.toPixels(constants.maxExpandDx); + maxExpandDy = scale.toPixels(constants.maxExpandDy); + maxMergeDx = scale.toPixels(constants.maxMergeDx); + maxMergeDy = scale.toPixels(constants.maxMergeDy); + maxMergeCenterDy = scale.toPixels(constants.maxMergeCenterDy); + clusterXMargin = scale.toPixels(constants.clusterXMargin); + clusterYMargin = scale.toPixels(constants.clusterYMargin); + + if (logger.isDebugEnabled()) { + Main.dumping.dump(this); + } + } + } +} diff --git a/src/main/omr/grid/Filament.java b/src/main/omr/grid/Filament.java new file mode 100644 index 0000000..b7f46ee --- /dev/null +++ b/src/main/omr/grid/Filament.java @@ -0,0 +1,224 @@ +//----------------------------------------------------------------------------// +// // +// F i l a m e n t // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.grid; + +import omr.Main; + +import omr.glyph.facets.BasicGlyph; +import omr.glyph.facets.GlyphComposition.Linking; + +import omr.lag.Section; + +import omr.run.Orientation; + +import omr.sheet.Scale; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Comparator; + +/** + * Class {@code Filament} represents a long glyph that can be far from + * being a straight line. + * It is used to handle candidate staff lines and bar lines. + */ +public class Filament + extends BasicGlyph +{ + //~ Static fields/initializers --------------------------------------------- + + /** Usual logger utility */ + private static final Logger logger = LoggerFactory.getLogger( + Filament.class); + + /** + * For comparing Filament instances on their top ordinate + */ + public static final Comparator topComparator = new Comparator() + { + @Override + public int compare (Filament f1, + Filament f2) + { + // Sort on top ordinate + return Integer.signum(f1.getBounds().y - f2.getBounds().y); + } + }; + + //~ Instance fields -------------------------------------------------------- + /** Related scale */ + private final Scale scale; + + //~ Constructors ----------------------------------------------------------- + //----------// + // Filament // + //----------// + /** + * Creates a new Filament object. + * + * @param scale scaling data + */ + public Filament (Scale scale) + { + this(scale, FilamentAlignment.class); + } + + //----------// + // Filament // + //----------// + /** + * Creates a new Filament object. + * + * @param scale scaling data + */ + public Filament (Scale scale, + Class alignmentClass) + { + super(scale.getInterline(), alignmentClass); + this.scale = scale; + } + + //~ Methods ---------------------------------------------------------------- + //------------// + // addSection // + //------------// + public void addSection (Section section) + { + addSection(section, Linking.LINK_BACK); + } + + //------------// + // addSection // + //------------// + @Override + public void addSection (Section section, + Linking link) + { + getComposition() + .addSection(section, link); + } + + //----------// + // deepDump // + //----------// + public void deepDump () + { + Main.dumping.dump(this); + Main.dumping.dump(getAlignment()); + } + + //------------------// + // getMeanCurvature // + //------------------// + public double getMeanCurvature () + { + return getAlignment() + .getMeanCurvature(); + } + + //----------// + // getScale // + //----------// + /** + * Report the scale that governs this filament. + * + * @return the related scale + */ + public Scale getScale () + { + return scale; + } + + //-----------------// + // polishCurvature // + //-----------------// + /** + * Polish the filament by looking at local curvatures and removing + * sections when necessary. + */ + public void polishCurvature () + { + getAlignment() + .polishCurvature(); + } + + //---------------// + // getPositionAt // + //---------------// + /** + * Report the precise filament position for the provided coordinate . + * + * @param coord the coord value (x for horizontal, y for vertical) + * @param orientation the reference orientation + * @return the pos value (y for horizontal, x for vertical) + */ + public double positionAt (double coord, + Orientation orientation) + { + return getAlignment() + .getPositionAt(coord, orientation); + } + + //---------// + // slopeAt // + //---------// + public double slopeAt (double coord, + Orientation orientation) + { + return getAlignment() + .slopeAt(coord, orientation); + } + + //------------// + // trueLength // + //------------// + /** + * Report an evaluation of how this filament is filled by sections + * + * @return how solid this filament is + */ + public int trueLength () + { + return (int) Math.rint((double) getWeight() / scale.getMainFore()); + } + + //--------------// + // getAlignment // + //--------------// + @Override + protected FilamentAlignment getAlignment () + { + return (FilamentAlignment) super.getAlignment(); + } + + //-----------------// + // internalsString // + //-----------------// + @Override + protected String internalsString () + { + StringBuilder sb = new StringBuilder(); + + if (getPartOf() != null) { + sb.append(" anc:") + .append(getAncestor()); + } + + if (getShape() != null) { + sb.append(" ") + .append(getShape()); + } + + return sb.toString(); + } +} diff --git a/src/main/omr/grid/FilamentAlignment.java b/src/main/omr/grid/FilamentAlignment.java new file mode 100644 index 0000000..e756190 --- /dev/null +++ b/src/main/omr/grid/FilamentAlignment.java @@ -0,0 +1,591 @@ +//----------------------------------------------------------------------------// +// // +// F i l a m e n t A l i g n m e n t // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.grid; + +import omr.constant.Constant; +import omr.constant.ConstantSet; + +import omr.glyph.facets.BasicAlignment; +import omr.glyph.facets.Glyph; +import omr.glyph.facets.GlyphComposition.Linking; + +import omr.lag.Section; + +import omr.math.LineUtil; +import omr.math.NaturalSpline; +import omr.math.Population; + +import omr.run.Orientation; + +import omr.sheet.Scale; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.awt.Graphics2D; +import java.awt.Rectangle; +import java.awt.geom.Ellipse2D; +import java.awt.geom.Line2D; +import java.awt.geom.Point2D; +import java.awt.geom.Rectangle2D; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; + +/** + * Class {@code FilamentAlignment} is a GlyphAlignment meant for a + * Filament instance, where the underlying Line is actually not a + * straight line, but a NaturalSpline. + * + * @author Hervé Bitteur + */ +public class FilamentAlignment + extends BasicAlignment +{ + //~ Static fields/initializers --------------------------------------------- + + /** Specific application parameters */ + private static final Constants constants = new Constants(); + + /** Usual logger utility */ + private static final Logger logger = LoggerFactory.getLogger( + FilamentAlignment.class); + + //~ Instance fields -------------------------------------------------------- + /** Absolute defining points */ + protected List points; + + /** Mean distance from a straight line */ + protected Double meanDistance; + + //~ Constructors ----------------------------------------------------------- + //-------------------// + // FilamentAlignment // + //-------------------// + /** + * Creates a new FilamentAlignment object. + */ + public FilamentAlignment (Glyph glyph) + { + super(glyph); + } + + //~ Methods ---------------------------------------------------------------- + //--------// + // dumpOf // + //--------// + @Override + public String dumpOf () + { + StringBuilder sb = new StringBuilder(); + + sb.append(super.dumpOf()); + sb.append(String.format(" meanRadius:%.3f%n", getMeanCurvature())); + + return sb.toString(); + } + + //---------// + // getLine // + //---------// + @Override + public NaturalSpline getLine () + { + return (NaturalSpline) super.getLine(); + } + + //------------------// + // getMeanCurvature // + //------------------// + /** + * Report the average radius of curvature along all segments of + * the curve. + * This is not a global radius, but rather a way to mesure how straight + * the curve is. + * + * @return the average of radius measurements along all curve segments + */ + public double getMeanCurvature () + { + Point2D prevPoint = null; + Line2D prevBisector = null; + Line2D bisector = null; + Population curvatures = new Population(); + + for (Point2D point : points) { + if (prevPoint != null) { + bisector = LineUtil.bisector( + new Line2D.Double(prevPoint, point)); + } + + if (prevBisector != null) { + Point2D inter = LineUtil.intersection( + prevBisector.getP1(), + prevBisector.getP2(), + bisector.getP1(), + bisector.getP2()); + double radius = Math.hypot( + inter.getX() - point.getX(), + inter.getY() - point.getY()); + + curvatures.includeValue(1 / radius); + } + + prevBisector = bisector; + prevPoint = point; + } + + if (curvatures.getCardinality() > 0) { + return 1 / curvatures.getMeanValue(); + } else { + return 0; + } + } + + //-----------------// + // getMeanDistance // + //-----------------// + @Override + public double getMeanDistance () + { + if (line == null) { + computeLine(); + } + + if (meanDistance == null) { + Line2D straight = new Line2D.Double(startPoint, stopPoint); + + double totalDistSq = 0; + int pointCount = points.size() - 2; // Only intermediate points! + + for (int i = 1, iMax = pointCount; i <= iMax; i++) { + totalDistSq += straight.ptLineDistSq(points.get(i)); + } + + if (pointCount > 0) { + meanDistance = Math.sqrt(totalDistSq / pointCount); + } + } + + return (meanDistance != null) ? meanDistance : 0; + } + + //---------------// + // getPositionAt // + //---------------// + @Override + public double getPositionAt (double coord, + Orientation orientation) + { + if (line == null) { + computeLine(); + } + + if (orientation == Orientation.HORIZONTAL) { + if ((coord < startPoint.getX()) || (coord > stopPoint.getX())) { + double sl = (stopPoint.getY() - startPoint.getY()) / (stopPoint. + getX() + - startPoint. + getX()); + + return startPoint.getY() + (sl * (coord - startPoint.getX())); + } else { + return line.yAtX(coord); + } + } else { + if ((coord < startPoint.getY()) || (coord > stopPoint.getY())) { + double sl = (stopPoint.getX() - startPoint.getX()) / (stopPoint. + getY() + - startPoint. + getY()); + + return startPoint.getX() + (sl * (coord - startPoint.getY())); + } else { + return line.xAtY(coord); + } + } + } + + //-----------------// + // invalidateCache // + //-----------------// + @Override + public void invalidateCache () + { + super.invalidateCache(); + points = null; + meanDistance = null; + } + + //-----------------// + // polishCurvature // + //-----------------// + /** + * Polish the filament by looking at local curvatures and removing + * sections when necessary. + *

If the local point is next to the first or last point of the curve, + * then the point to modify is likely to be this first or last point. + * In the other cases, the local point itself is modified. + */ + public void polishCurvature () + { + boolean modified = false; + + do { + modified = false; + + final List bisectors = getBisectors(); + + // Compute radius values (using same index as points) + final List radii = new ArrayList<>(); + radii.add(null); // To skip index 0 for which we have no value (???) + + for (int i = 1, iBreak = points.size() - 1; i < iBreak; i++) { + radii.add(getRadius(i, bisectors)); + } + + // Check smallest radius + Integer idx = null; + double minRadius = Integer.MAX_VALUE; + + for (int i = 1, iBreak = points.size() - 1; i < iBreak; i++) { + double radius = radii.get(i); + + if (minRadius > radius) { + minRadius = radius; + idx = i; + } + } + + double rad = minRadius / glyph.getInterline(); + + if (rad < constants.minRadius.getValue()) { + if (logger.isDebugEnabled() || glyph.isVip()) { + logger.info("Polishing F#{} minRad: {} seq:{} {}", + glyph.getId(), (float) rad, idx, points.get(idx)); + } + + // Adjust the removable point for first & last points + if (idx == 1) { + idx--; + } else if (idx == (points.size() - 2)) { + idx++; + } + + // Lookup corresponding section(s) + Scale scale = new Scale(glyph.getInterline()); + int probeWidth = scale.toPixels( + BasicAlignment.getProbeWidth()); + Orientation orientation = getRoughOrientation(); + final Point2D point = points.get(idx); + Point2D orientedPt = orientation.oriented( + points.get(idx)); + Rectangle2D rect = new Rectangle2D.Double( + orientedPt.getX() - (probeWidth / 2), + orientedPt.getY() - (probeWidth / 2), + probeWidth, + probeWidth); + List

found = new ArrayList<>(); + + for (Section section : glyph.getMembers()) { + if (rect.intersects(section.getOrientedBounds())) { + found.add(section); + } + } + + if (found.size() > 1) { + // Pick up the section closest to the point + Collections.sort( + found, + new Comparator
() + { + @Override + public int compare (Section s1, + Section s2) + { + return Double.compare( + point.distance(s1.getCentroid()), + point.distance(s2.getCentroid())); + } + }); + } + + Section section = found.isEmpty() ? null : found.get(0); + + if (section != null) { + logger.debug("Removed section#{} from {} F{}", + section.getId(), orientation, glyph.getId()); + glyph.removeSection(section, Linking.LINK_BACK); + modified = true; + } + } + } while (modified); + } + + //------------// + // renderLine // + //------------// + @Override + public void renderLine (Graphics2D g) + { + if (!glyph.getBounds().intersects(g.getClipBounds())) { + return; + } + + // The curved line itself + if (line != null) { + g.draw((NaturalSpline) line); + } + + // Then the absolute defining points? + if (constants.showFilamentPoints.isSet() && (points != null)) { + // Point radius + double r = glyph.getInterline() * constants.filamentPointSize. + getValue(); + Ellipse2D ellipse = new Ellipse2D.Double(); + + for (Point2D p : points) { + ellipse.setFrame(p.getX() - r, p.getY() - r, 2 * r, 2 * r); + g.fill(ellipse); + } + } + } + + //---------// + // slopeAt // + //---------// + public double slopeAt (double coord, + Orientation orientation) + { + if (line == null) { + computeLine(); + } + + if (orientation == Orientation.HORIZONTAL) { + return getLine().yDerivativeAtX(coord); + } else { + return getLine().xDerivativeAtY(coord); + } + } + + //-------------// + // computeLine // + //-------------// + /** + * Compute cached data: curve, startPoint, stopPoint, slope. + * Curve goes from startPoint to stopPoint through intermediate points + * regularly spaced + */ + @Override + protected void computeLine () + { + try { + Scale scale = new Scale(glyph.getInterline()); + + /** Width of window to retrieve pixels */ + int probeWidth = scale.toPixels(BasicAlignment.getProbeWidth()); + + /** Typical length of curve segments */ + double typicalLength = scale.toPixels(constants.segmentLength); + + // We need a rough orientation right now + Orientation orientation = getRoughOrientation(); + Point2D orientedStart = (startPoint == null) ? null + : orientation.oriented(startPoint); + Point2D orientedStop = (stopPoint == null) ? null + : orientation.oriented(stopPoint); + Rectangle oBounds = orientation.oriented(glyph.getBounds()); + double oStart = (orientedStart != null) ? orientedStart.getX() + : oBounds.x; + double oStop = (orientedStop != null) ? orientedStop.getX() + : (oBounds.x + (oBounds.width - 1)); + double length = oStop - oStart + 1; + + Rectangle oProbe = new Rectangle(oBounds); + oProbe.x = (int) Math.ceil(oStart); + oProbe.width = probeWidth; + + // Determine the number of segments and their precise length + int segCount = (int) Math.rint(length / typicalLength); + double segLength = length / segCount; + List newPoints = new ArrayList<>(segCount + 1); + + // First point + if (startPoint == null) { + Point2D p = orientation.oriented( + getRectangleCentroid(orientation.absolute(oProbe))); + startPoint = orientation.absolute( + new Point2D.Double(oStart, p.getY())); + } + + newPoints.add(startPoint); + + // Intermediate points (perhaps none) + for (int i = 1; i < segCount; i++) { + oProbe.x = (int) Math.rint(oStart + (i * segLength)); + + Point2D pt = getRectangleCentroid(orientation.absolute(oProbe)); + + // If, unfortunately, we are in a filament hole, just skip it + if (pt != null) { + newPoints.add(pt); + } + } + + // Last point + if (stopPoint == null) { + oProbe.x = (int) Math.floor(oStop - oProbe.width + 1); + + Point2D p = orientation.oriented( + getRectangleCentroid(orientation.absolute(oProbe))); + stopPoint = orientation.absolute( + new Point2D.Double(oStop, p.getY())); + } + + newPoints.add(stopPoint); + + // Interpolate the best spline through the provided points + line = NaturalSpline.interpolate( + newPoints.toArray(new Point2D[newPoints.size()])); + + // Remember points (atomically) + this.points = newPoints; + + // Cache global slope + getSlope(); + } catch (Exception ex) { + logger.warn("Filament cannot computeData", ex); + } + } + + //-----------// + // findPoint // + //-----------// + protected Point2D findPoint (int coord, + Orientation orientation, + int margin) + { + Point2D best = null; + double bestDeltacoord = Integer.MAX_VALUE; + + for (Point2D p : points) { + double dc = Math.abs( + coord + - ((orientation == Orientation.HORIZONTAL) ? p.getX() : p. + getY())); + + if ((dc <= margin) && (dc < bestDeltacoord)) { + bestDeltacoord = dc; + best = p; + } + } + + return best; + } + + //--------------// + // getBisectors // + //--------------// + /** + * Report bisectors of inter-points segments. + * + * @return sequence of bisectors, such that bisectors[i] is bisector of + * segment (i -> i+1) + */ + private List getBisectors () + { + if (line == null) { + computeLine(); + } + + List bisectors = new ArrayList<>(); + + for (int i = 0; i < (points.size() - 1); i++) { + bisectors.add( + LineUtil.bisector( + new Line2D.Double(points.get(i), points.get(i + 1)))); + } + + return bisectors; + } + + //-----------// + // getRadius // + //-----------// + /** + * Report radius computed at point with index 'i'. + *

TODO: This a simplistic way for computing radius, based on insection + * of the two adjacent bisectors. + * There may be other ways, such as using the following property: + * sin angle a / length of segment a = 1 / (2 * radius) + * + * @param i the index of desired point + * @param bisectors the sequence of bisectors + * @return the value of radius of curvature + */ + private double getRadius (int i, + List bisectors) + { + Line2D prevBisector = bisectors.get(i - 1); + Point2D point = points.get(i); + Line2D nextBisector = bisectors.get(i); + + Point2D inter = LineUtil.intersection( + prevBisector.getP1(), + prevBisector.getP2(), + nextBisector.getP1(), + nextBisector.getP2()); + + return Math.hypot( + inter.getX() - point.getX(), + inter.getY() - point.getY()); + } + + //---------------------// + // getRoughOrientation // + //---------------------// + private Orientation getRoughOrientation () + { + Rectangle box = glyph.getBounds(); + + return (box.height > box.width) ? Orientation.VERTICAL + : Orientation.HORIZONTAL; + } + + //~ Inner Classes ---------------------------------------------------------- + //-----------// + // Constants // + //-----------// + private static final class Constants + extends ConstantSet + { + //~ Instance fields ---------------------------------------------------- + + final Scale.Fraction segmentLength = new Scale.Fraction( + 2, + "Typical length between filament curve intermediate points"); + + Constant.Boolean showFilamentPoints = new Constant.Boolean( + false, + "Should we display filament points?"); + + Scale.Fraction filamentPointSize = new Scale.Fraction( + 0.05, + "Size of displayed filament points"); + + Scale.Fraction minRadius = new Scale.Fraction( + 12, + "Minimum acceptable radius of curvature"); + + } +} diff --git a/src/main/omr/grid/FilamentComb.java b/src/main/omr/grid/FilamentComb.java new file mode 100644 index 0000000..d91c22b --- /dev/null +++ b/src/main/omr/grid/FilamentComb.java @@ -0,0 +1,180 @@ +//----------------------------------------------------------------------------// +// // +// F i l a m e n t C o m b // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.grid; + +import java.util.ArrayList; +import java.util.List; + +/** + * Class {@code FilamentComb} describe a series of y values + * corresponding to horizontal filaments rather regularly separated. + * + * @author Hervé Bitteur + */ +public class FilamentComb +{ + //~ Instance fields -------------------------------------------------------- + + /** Column index where sample was taken */ + private final int col; + + /** Series of filaments involved */ + private final List filaments; + + /** Ordinate value for each filament */ + private final List ys; + + /** To save processing */ + private boolean processed = false; + + //~ Constructors ----------------------------------------------------------- + //-----------------// + // FilamentComb // + //-----------------// + /** + * Creates a new FilamentComb object. + * + * @param col the column index + */ + public FilamentComb (int col) + { + this.col = col; + + filaments = new ArrayList<>(); + ys = new ArrayList<>(); + } + + //~ Methods ---------------------------------------------------------------- + //--------// + // append // + //--------// + /** + * Append a filament to the series. + * + * @param filament the filament to append + * @param y the filament ordinate at x abscissa + */ + public void append (LineFilament filament, + double y) + { + filaments.add(filament); + ys.add(y); + filament.addComb(col, this); // Link back Fil -> Comb + } + + //----------// + // getCount // + //----------// + /** + * Report the number of filaments in this series. + * + * @return the count + */ + public int getCount () + { + return filaments.size(); + } + + //-------------// + // getFilament // + //-------------// + public LineFilament getFilament (int index) + { + return filaments.get(index); + } + + //--------------// + // getFilaments // + //--------------// + public List getFilaments () + { + return filaments; + } + + //----------// + // getIndex // + //----------// + public int getIndex (LineFilament filament) + { + LineFilament ancestor = (LineFilament) filament.getAncestor(); + + for (int index = 0; index < filaments.size(); index++) { + LineFilament fil = filaments.get(index); + + if (fil.getAncestor() == ancestor) { + return index; + } + } + + return -1; + } + + //------// + // getY // + //------// + public double getY (int index) + { + return ys.get(index); + } + + //-------------// + // isProcessed // + //-------------// + /** + * @return the processed + */ + public boolean isProcessed () + { + return processed; + } + + //--------------// + // setProcessed // + //--------------// + /** + * @param processed the processed to set + */ + public void setProcessed (boolean processed) + { + this.processed = processed; + } + + //----------// + // toString // + //----------// + @Override + public String toString () + { + StringBuilder sb = new StringBuilder("{"); + sb.append("Pattern"); + + sb.append(" col:") + .append(col); + + sb.append(" ") + .append(filaments.size()); + + for (int i = 0; i < filaments.size(); i++) { + LineFilament fil = (LineFilament) filaments.get(i) + .getAncestor(); + double y = ys.get(i); + sb.append(" F#") + .append(fil.getId()) + .append("@") + .append((float) y); + } + + sb.append("}"); + + return sb.toString(); + } +} diff --git a/src/main/omr/grid/FilamentLine.java b/src/main/omr/grid/FilamentLine.java new file mode 100644 index 0000000..875a846 --- /dev/null +++ b/src/main/omr/grid/FilamentLine.java @@ -0,0 +1,292 @@ +//----------------------------------------------------------------------------// +// // +// F i l a m e n t L i n e // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.grid; + +import omr.lag.Section; + +import omr.math.Line; +import omr.math.LineUtil; + +import omr.run.Orientation; + +import omr.util.HorizontalSide; +import static omr.util.HorizontalSide.*; + +import java.awt.Graphics2D; +import java.awt.Rectangle; +import java.awt.geom.Point2D; +import java.util.Collection; + +/** + * Class {@code FilamentLine} implements a staff line (or a part of it), + * based on filaments + * + * @author Hervé Bitteur + */ +public class FilamentLine + implements LineInfo +{ + //~ Instance fields -------------------------------------------------------- + + /** Underlying filament */ + LineFilament fil; + + //~ Constructors ----------------------------------------------------------- + //--------------// + // FilamentLine // + //--------------// + /** + * Creates a new FilamentLine object. + * + * @param fil the initial filament to add + */ + public FilamentLine (LineFilament fil) + { + add(fil); + } + + //~ Methods ---------------------------------------------------------------- + //-----// + // add // + //-----// + public final void add (LineFilament fil) + { + if (this.fil == null) { + this.fil = fil; + } else { + this.fil.include(fil); + } + } + + //-----------// + // getBounds // + //-----------// + @Override + public Rectangle getBounds () + { + return fil.getBounds(); + } + + //-------------// + // getEndPoint // + //-------------// + @Override + public Point2D getEndPoint (HorizontalSide side) + { + if (side == HorizontalSide.LEFT) { + return getStartPoint(); + } else { + return getStopPoint(); + } + } + + //-------------// + // getFilament // + //-------------// + public LineFilament getFilament () + { + return fil; + } + + //-------// + // getId // + //-------// + @Override + public int getId () + { + return fil.getId(); + } + + //--------------// + // getLeftPoint // + //--------------// + @Override + public Point2D getLeftPoint () + { + return getStartPoint(); + } + + //---------------// + // getRightPoint // + //---------------// + @Override + public Point2D getRightPoint () + { + return getStopPoint(); + } + + //-------------// + // getSections // + //-------------// + @Override + public Collection

getSections () + { + throw new UnsupportedOperationException("Not supported yet."); + } + + //----------// + // getSlope // + //----------// + public double getSlope (HorizontalSide side) + { + return fil.slopeAt(getEndPoint(side).getX(), Orientation.HORIZONTAL); + } + + //---------------// + // getStartPoint // + //---------------// + public Point2D getStartPoint () + { + return fil.getStartPoint(Orientation.HORIZONTAL); + } + + //---------------// + // getStartSlope // + //---------------// + public double getStartSlope () + { + return fil.slopeAt(getStartPoint().getX(), Orientation.HORIZONTAL); + } + + //--------------// + // getStopPoint // + //--------------// + public Point2D getStopPoint () + { + return fil.getStopPoint(Orientation.HORIZONTAL); + } + + //--------------// + // getStopSlope // + //--------------// + public double getStopSlope () + { + return fil.slopeAt(getStopPoint().getX(), Orientation.HORIZONTAL); + } + + //---------// + // include // + //---------// + public void include (FilamentLine that) + { + add(that.fil); + } + + //---------------// + // isWithinRange // + //---------------// + /** + * Report whether the provided abscissa lies within the line range + * + * @param x the provided abscissa + * @return true if within range + */ + public boolean isWithinRange (double x) + { + return (x >= getStartPoint() + .getX()) && (x <= getStopPoint() + .getX()); + } + + //--------// + // render // + //--------// + public void render (Graphics2D g, + int left, + int right) + { + // Ignore left and right + render(g); + } + + //--------// + // render // + //--------// + @Override + public void render (Graphics2D g) + { + fil.renderLine(g); + } + + //-----------------// + // setEndingPoints // + //-----------------// + public void setEndingPoints (Point2D pStart, + Point2D pStop) + { + fil.setEndingPoints(pStart, pStop); + } + + //----------// + // toString // + //----------// + @Override + public String toString () + { + StringBuilder sb = new StringBuilder("Line#"); + sb.append(fil.getClusterPos()); + sb.append("["); + + sb.append("F") + .append(fil.getId()); + + sb.append("]"); + + sb.append(fil.trueLength()); + + return sb.toString(); + } + + //----------------------// + // verticalIntersection // + //----------------------// + @Override + public Point2D verticalIntersection (Line vertical) + { + // We need two points on the rather vertical line + Point2D startPoint = new Point2D.Double(vertical.xAtY(0.0), 0.0); + Point2D stopPoint = new Point2D.Double(vertical.xAtY(1000.0), 1000.0); + + // First, get a rough intersection + Point2D pt = LineUtil.intersection( + getEndPoint(LEFT), + getEndPoint(RIGHT), + startPoint, + stopPoint); + + // Second, get a precise ordinate + double y = yAt(pt.getX()); + + // Third, get a precise abscissa + double x = vertical.xAtY(y); + + return new Point2D.Double(x, y); + } + + //-----// + // yAt // + //-----// + @Override + public int yAt (int x) + { + return (int) Math.rint(yAt((double) x)); + } + + //-----// + // yAt // + //-----// + @Override + public double yAt (double x) + { + return fil.getPositionAt(x, Orientation.HORIZONTAL); + } +} diff --git a/src/main/omr/grid/FilamentsFactory.java b/src/main/omr/grid/FilamentsFactory.java new file mode 100644 index 0000000..dc85891 --- /dev/null +++ b/src/main/omr/grid/FilamentsFactory.java @@ -0,0 +1,1007 @@ +//----------------------------------------------------------------------------// +// // +// F i l a m e n t s F a c t o r y // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.grid; + +import omr.Main; + +import omr.constant.Constant; +import omr.constant.ConstantSet; + +import omr.glyph.Glyphs; +import omr.glyph.Nest; +import omr.glyph.facets.BasicAlignment; +import omr.glyph.facets.BasicGlyph; +import omr.glyph.facets.Glyph; +import omr.glyph.facets.GlyphComposition; + +import omr.lag.Section; +import omr.lag.Sections; + +import omr.math.Line; +import omr.math.PointsCollector; + +import omr.run.Orientation; + +import omr.sheet.Scale; + +import omr.util.StopWatch; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.awt.Rectangle; +import java.awt.geom.Point2D; +import java.lang.reflect.Constructor; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; + +/** + * Class {@code FilamentsFactory} builds filaments (long series of + * sections) out of a collection of sections. + * + *

These filaments are meant to represent good candidates for (horizontal) + * staff lines or (vertical) bar lines. The factory aims at a given orientation, + * though the input sections may exhibit mixed orientations.

+ * + *

Internal parameters have default values defined via a ConstantSet. Before + * launching filaments retrieval by {@link #retrieveFilaments}, parameters can + * be modified individually by calling some setXXX() methods.

+ * + * @author Hervé Bitteur + */ +public class FilamentsFactory +{ + //~ Static fields/initializers --------------------------------------------- + + /** Specific application parameters */ + private static final Constants constants = new Constants(); + + /** Usual logger utility */ + private static final Logger logger = LoggerFactory.getLogger( + FilamentsFactory.class); + + //~ Instance fields -------------------------------------------------------- + /** Related scale */ + private final Scale scale; + + /** Where filaments are to be stored */ + private final Nest nest; + + /** Factory orientation */ + private final Orientation orientation; + + /** Precise constructor for filaments */ + private final Constructor filamentConstructor; + + private final Object[] scaleArgs; + + /** Scale-dependent constants for horizontal stuff */ + private final Parameters params; + + /** Long filaments found, non sorted */ + private final List filaments = new ArrayList<>(); + + //~ Constructors ----------------------------------------------------------- + //------------------// + // FilamentsFactory // + //------------------// + /** + * Create a factory of filaments. + * + * @param scale the related scale + * @param nest the nest to host created filaments + * @param orientation the target orientation + * @param filamentClass precise Filament class to be use for creation + * @throws Exception + */ + public FilamentsFactory (Scale scale, + Nest nest, + Orientation orientation, + Class filamentClass) + throws Exception + { + this.scale = scale; + this.nest = nest; + this.orientation = orientation; + + scaleArgs = new Object[]{scale}; + filamentConstructor = filamentClass.getConstructor( + new Class[]{Scale.class}); + + params = new Parameters(); + params.initialize(); + } + + //~ Methods ---------------------------------------------------------------- + //------// + // dump // + //------// + public void dump () + { + params.dump(); + } + + //--------------// + // isSectionFat // + //--------------// + /** + * Detect if the provided section is a thick one. + * (as seen in the context of the factory orientation) + * + * @param section the section to check + * @return true if fat + */ + public boolean isSectionFat (Section section) + { + if (section.isFat() == null) { + try { + if (section.getMeanThickness(orientation) <= 1) { + section.setFat(false); + + return section.isFat(); + } + + // Check global slimness + if (section.getMeanAspect(orientation) < params.minSectionAspect) { + section.setFat(true); + + return section.isFat(); + } + + // Check thickness + Rectangle bounds = orientation.oriented(section.getBounds()); + Line line = orientation.switchRef( + section.getAbsoluteLine()); + + if (Math.abs(line.getSlope()) < (Math.PI / 4)) { + // Measure mean thickness on each half + int startCoord = bounds.x + (bounds.width / 4); + int startPos = line.yAtX(startCoord); + int stopCoord = bounds.x + ((3 * bounds.width) / 4); + int stopPos = line.yAtX(stopCoord); + + // Start side + Rectangle oRoi = new Rectangle(startCoord, startPos, 0, 0); + final int halfWidth = Math.min( + params.probeWidth / 2, + bounds.width / 4); + oRoi.grow(halfWidth, params.maxSectionThickness); + + PointsCollector collector = new PointsCollector( + orientation.absolute(oRoi)); + section.cumulate(collector); + + int startThickness = (int) Math.rint( + (double) collector.getSize() / oRoi.width); + + // Stop side + oRoi.translate(stopCoord - startCoord, stopPos - startPos); + collector = new PointsCollector(orientation.absolute(oRoi)); + section.cumulate(collector); + + int stopThickness = (int) Math.rint( + (double) collector.getSize() / oRoi.width); + + section.setFat( + (startThickness > params.maxSectionThickness) + || (stopThickness > params.maxSectionThickness)); + } else { + section.setFat(bounds.height > params.maxSectionThickness); + } + } catch (Exception ex) { + logger.warn("Error in checking fatness of " + section, ex); + section.setFat(true); + } + } + + return section.isFat(); + } + + //-------------------// + // retrieveFilaments // + //-------------------// + /** + * Aggregate the long and thin sections into filaments (glyphs). + * + * @param source the section source for filaments + * @param useExpansion true to expand filaments with short sections left + * over + * @return the collection of retrieved filaments + */ + public List retrieveFilaments (Collection
source, + boolean useExpansion) + { + StopWatch watch = new StopWatch("FilamentsFactory"); + + try { + // Create a filament for each section long & slim + watch.start("createFilaments"); + createFilaments(source); + + logger.debug("{} {} filaments created.", + orientation, filaments.size()); + + // Merge filaments into larger filaments + watch.start("mergeFilaments"); + mergeFilaments(); + + // Expand with short sections left over? + if (useExpansion) { + watch.start("expandFilaments"); + expandFilaments(source); + + // Merge filaments into larger filaments + watch.start("mergeFilaments #2"); + mergeFilaments(); + } + } catch (Exception ex) { + logger.warn("FilamentsFactory cannot retrieveFilaments", ex); + } finally { + if (constants.printWatch.getValue()) { + watch.print(); + } + } + + return filaments; + } + + //----------------// + // setMaxCoordGap // + //----------------// + public void setMaxCoordGap (Scale.Fraction frac) + { + params.maxCoordGap = scale.toPixels(frac); + } + + //----------------------// + // setMaxExpansionSpace // + //----------------------// + public void setMaxExpansionSpace (Scale.Fraction frac) + { + params.maxExpansionSpace = scale.toPixels(frac); + } + + //-------------------------// + // setMaxFilamentThickness // + //-------------------------// + public void setMaxFilamentThickness (Scale.LineFraction lineFrac) + { + params.maxFilamentThickness = scale.toPixels(lineFrac); + } + + //-------------------------// + // setMaxFilamentThickness // + //-------------------------// + public void setMaxFilamentThickness (Scale.Fraction frac) + { + params.maxFilamentThickness = scale.toPixels(frac); + } + + //----------------// + // setMaxGapSlope // + //----------------// + public void setMaxGapSlope (double value) + { + params.maxGapSlope = value; + } + + //-----------------------// + // setMaxInvolvingLength // + //-----------------------// + public void setMaxInvolvingLength (Scale.Fraction frac) + { + params.maxInvolvingLength = scale.toPixels(frac); + } + + //-----------------------// + // setMaxOverlapDeltaPos // + //-----------------------// + public void setMaxOverlapDeltaPos (Scale.Fraction frac) + { + params.maxOverlapDeltaPos = scale.toPixels(frac); + } + + //-----------------------// + // setMaxOverlapDeltaPos // + //-----------------------// + public void setMaxOverlapDeltaPos (Scale.LineFraction lFrac) + { + params.maxOverlapDeltaPos = scale.toPixels(lFrac); + } + + //--------------// + // setMaxPosGap // + //--------------// + public void setMaxPosGap (Scale.LineFraction lineFrac) + { + params.maxPosGap = scale.toPixels(lineFrac); + } + + //--------------// + // setMaxPosGap // + //--------------// + public void setMaxPosGap (Scale.Fraction frac) + { + params.maxPosGap = scale.toPixels(frac); + } + + //----------------------// + // setMaxPosGapForSlope // + //----------------------// + public void setMaxPosGapForSlope (Scale.Fraction frac) + { + params.maxPosGapForSlope = scale.toPixels(frac); + } + + //------------------------// + // setMaxSectionThickness // + //------------------------// + public void setMaxSectionThickness (Scale.LineFraction lineFrac) + { + params.maxSectionThickness = scale.toPixels(lineFrac); + } + + //------------------------// + // setMaxSectionThickness // + //------------------------// + public void setMaxSectionThickness (Scale.Fraction frac) + { + params.maxSectionThickness = scale.toPixels(frac); + } + + //-------------// + // setMaxSpace // + //-------------// + public void setMaxSpace (Scale.Fraction frac) + { + params.maxSpace = scale.toPixels(frac); + } + + //-------------------------// + // setMinCoreSectionLength // + //-------------------------// + public void setMinCoreSectionLength (Scale.Fraction frac) + { + setMinCoreSectionLength(scale.toPixels(frac)); + } + + //-------------------------// + // setMinCoreSectionLength // + //-------------------------// + public void setMinCoreSectionLength (int value) + { + params.minCoreSectionLength = value; + } + + //---------------------// + // setMinSectionAspect // + //---------------------// + public void setMinSectionAspect (double value) + { + params.minSectionAspect = value; + } + + //----------// + // canMerge // + //----------// + /** + * Check whether the two provided filaments could be merged. + * + * @param one a filament + * @param two another filament + * @param expanding true when expanding filaments with sections left over + * @return true if test is positive + */ + private boolean canMerge (Glyph one, + Glyph two, + boolean expanding) + { + // For VIP debugging + final boolean areVips = one.isVip() && two.isVip(); + String vips = null; + + if (areVips) { + vips = one.getId() + "&" + two.getId() + ": "; // BP here! + } + + try { + // Start & Stop points for each filament + Point2D oneStart = orientation.oriented( + one.getStartPoint(orientation)); + Point2D oneStop = orientation.oriented( + one.getStopPoint(orientation)); + Point2D twoStart = orientation.oriented( + two.getStartPoint(orientation)); + Point2D twoStop = orientation.oriented( + two.getStopPoint(orientation)); + + // coord gap? + double overlapStart = Math.max(oneStart.getX(), twoStart.getX()); + double overlapStop = Math.min(oneStop.getX(), twoStop.getX()); + double coordGap = (overlapStart - overlapStop) - 1; + + if (coordGap > params.maxCoordGap) { + if (logger.isDebugEnabled() || areVips) { + logger.info( + "{}Gap too long: {} vs {}", + vips, coordGap, params.maxCoordGap); + } + + return false; + } + + // pos gap? + if (coordGap < 0) { + // Overlap between the two filaments + // Determine maximum consistent resulting thickness + double maxConsistentThickness = maxConsistentThickness(one); + double maxSpace = expanding ? params.maxExpansionSpace + : params.maxSpace; + + // Measure thickness at various coord values of overlap + // Provided that the overlap is long enough + int valNb = (int) Math.min(3, 1 - (coordGap / 10)); + + for (int iq = 1; iq <= valNb; iq++) { + double midCoord = overlapStart + - ((iq * coordGap) / (valNb + 1)); + double onePos = one.getPositionAt(midCoord, orientation); + double twoPos = two.getPositionAt(midCoord, orientation); + double posGap = Math.abs(onePos - twoPos); + + if (posGap > params.maxOverlapDeltaPos) { + if (logger.isDebugEnabled() || areVips) { + logger.info( + "{}Delta pos too high for overlap: {} vs {}", + vips, posGap, params.maxOverlapDeltaPos); + } + + return false; + } + + // Check resulting thickness at middle of overlap + double thickness = Glyphs.getThicknessAt( + midCoord, + orientation, + one, + two); + + if (thickness > params.maxFilamentThickness) { + if (logger.isDebugEnabled() || areVips) { + logger.info( + "{}Too thick: {} vs {} {} {}", + vips, (float) thickness, + params.maxFilamentThickness, one, two); + } + + return false; + } + + // Check thickness consistency + if ((-coordGap <= params.maxInvolvingLength) + && (thickness > maxConsistentThickness)) { + if (logger.isDebugEnabled() || areVips) { + logger.info( + "{}Non consistent thickness: {} vs {} {} {}", + vips, (float) thickness, + (float) maxConsistentThickness, one, two); + } + + return false; + } + + // Check space between overlapped filaments + double space = thickness + - (one.getThicknessAt(midCoord, orientation) + + two.getThicknessAt(midCoord, orientation)); + + if (space > maxSpace) { + if (logger.isDebugEnabled() || areVips) { + logger.info( + "{}Space too large: {} vs {} {} {}", + vips, (float) space, maxSpace, one, two); + } + + return false; + } + } + } else { + // No overlap, it's a true gap + Point2D start; + Point2D stop; + + if (oneStart.getX() < twoStart.getX()) { + // one - two + start = oneStop; + stop = twoStart; + } else { + // two - one + start = twoStop; + stop = oneStart; + } + + // Compute position gap, taking thickness into account + double oneThickness = one.getWeight() / one.getLength( + orientation); + double twoThickness = two.getWeight() / two.getLength( + orientation); + int posMargin = (int) Math.rint( + Math.max(oneThickness, twoThickness) / 2); + double posGap = Math.abs(stop.getY() - start.getY()) + - posMargin; + + if (posGap > params.maxPosGap) { + if (logger.isDebugEnabled() || areVips) { + logger.info( + "{}Delta pos too high for gap: {} vs {}", + vips, (float) posGap, params.maxPosGap); + } + + return false; + } + + // Check slope (relevant only for significant dy) + if (posGap > params.maxPosGapForSlope) { + double gapSlope = posGap / coordGap; + + if (gapSlope > params.maxGapSlope) { + if (logger.isDebugEnabled() || areVips) { + logger.info( + "{}Slope too high for gap: {} vs {}", + vips, (float) gapSlope, params.maxGapSlope); + } + + return false; + } + } + } + + if (logger.isDebugEnabled() || areVips) { + logger.info("{}Compatible!", vips); + } + + return true; + } catch (Exception ex) { + // Generally a stick for which some parameters cannot be computed + return false; + } + } + + //----------------// + // createFilament // + //----------------// + private Glyph createFilament (Section section) + throws Exception + { + Filament fil = (Filament) filamentConstructor.newInstance(scaleArgs); + fil.addSection(section); + + return nest.addGlyph(fil); + } + + //-----------------// + // createFilaments // + //-----------------// + /** + * Aggregate long sections into initial filaments. + */ + private void createFilaments (Collection
source) + throws Exception + { + // Sort sections by decreasing length in the desired orientation + List
sections = new ArrayList<>(source); + Collections.sort( + sections, + Sections.getReverseLengthComparator(orientation)); + + for (Section section : sections) { + // Limit to main sections + if (section.getLength(orientation) < params.minCoreSectionLength) { + if (section.isVip()) { + logger.info("Too short {}", section); + } + + continue; + } + + if (isSectionFat(section)) { + if (section.isVip()) { + logger.info("Too fat {}", section); + } + + continue; + } + + Glyph fil = createFilament(section); + filaments.add(fil); + + if (logger.isDebugEnabled() || section.isVip() || nest.isVip(fil)) { + logger.info( + "Created {} with {}", fil, section); + + if (section.isVip() || nest.isVip(fil)) { + fil.setVip(); + } + } + } + + logger.debug("createFilaments: {}/{}", filaments.size(), source.size()); + } + + //-----------------// + // expandFilaments // + //-----------------// + /** + * Expand as much as possible the existing filaments with the + * provided sections. + * + * @param source the source of available sections + * @return the collection of expanded filaments + */ + private List expandFilaments (Collection
source) + { + try { + // Sort sections by first position + List
sections = new ArrayList<>(); + + for (Section section : source) { + if (!section.isGlyphMember() && !isSectionFat(section)) { + sections.add(section); + } + } + + logger.debug("expandFilaments: {}/{}", + sections.size(), source.size()); + + Collections.sort(sections, Section.posComparator); + + List glyphs = new ArrayList<>(sections.size()); + + for (Section section : sections) { + Glyph glyph = new BasicGlyph(scale.getInterline()); + glyph.addSection( + section, + GlyphComposition.Linking.NO_LINK_BACK); + glyph = nest.addGlyph(glyph); + glyphs.add(glyph); + + if (section.isVip() || nest.isVip(glyph)) { + logger.info("VIP created {} from {}", glyph, section); + glyph.setVip(); + } + } + + // List of filaments, sorted by decreasing length + Collections.sort( + filaments, + Glyphs.getReverseLengthComparator(orientation)); + + // Process each filament on turn + for (Glyph fil : filaments) { + // Build filament fat box + final Rectangle filBounds = orientation.oriented( + fil.getBounds()); + filBounds.grow(params.maxCoordGap, params.maxPosGap); + + boolean expanding = true; + + do { + expanding = false; + + for (Iterator it = glyphs.iterator(); it.hasNext();) { + Glyph glyph = it.next(); + Rectangle glyphBounds = orientation.oriented( + glyph.getBounds()); + + if (filBounds.intersects(glyphBounds)) { + // Check more closely + if (canMerge(fil, glyph, true)) { + if (logger.isDebugEnabled() + || fil.isVip() + || glyph.isVip()) { + logger.info("Merging {} w/ {}", + fil, + Sections.toString(glyph.getMembers())); + + if (glyph.isVip()) { + fil.setVip(); + } + } + + fil.stealSections(glyph); + it.remove(); + expanding = true; + + break; + } + } else { + if (fil.isVip() && glyph.isVip()) { + logger.info("No intersection between {} and {}", + fil, glyph); + } + } + } + } while (expanding); + } + } catch (Exception ex) { + logger.warn("FilamentsFactory cannot expandFilaments", ex); + } + + return filaments; + } + + //------------------------// + // maxConsistentThickness // + //------------------------// + private double maxConsistentThickness (Glyph stick) + { + double mean = stick.getWeight() / (double) stick.getLength(orientation); + + if (mean < 2) { + return 2 * constants.maxConsistentRatio.getValue() * mean; + } else { + return constants.maxConsistentRatio.getValue() * mean; + } + } + + //----------------// + // mergeFilaments // + //----------------// + /** + * Aggregate single-section filaments into long multi-section + * filaments. + */ + private void mergeFilaments () + { + // List of filaments, sorted by decreasing length + Collections.sort( + filaments, + Glyphs.getReverseLengthComparator(orientation)); + + // Browse by decreasing filament length + for (Glyph current : filaments) { + Glyph candidate = current; + + // Keep on working while we do have a candidate to check for merge + CandidateLoop: + while (true) { + final Rectangle candidateBounds = orientation.oriented( + candidate.getBounds()); + candidateBounds.grow(params.maxCoordGap, params.maxPosGap); + + // Check the candidate vs all filaments until current excluded + HeadsLoop: + for (Glyph head : filaments) { + if (head == current) { + break CandidateLoop; // Actual end of sub-list + } + + if ((head != candidate) && (head.getPartOf() == null)) { + Rectangle headBounds = orientation.oriented( + head.getBounds()); + + if (headBounds.intersects(candidateBounds)) { + // Check for a possible merge + if (canMerge(head, candidate, false)) { + if (logger.isDebugEnabled() + || head.isVip() + || candidate.isVip()) { + logger.info( + "Merged {} into {}", + candidate, head); + + if (candidate.isVip()) { + head.setVip(); + } + } + + head.stealSections(candidate); + candidate = head; // This is a new candidate + + break HeadsLoop; + } else { + // if (head.isVip() || candidate.isVip()) { + // logger.info( + // "Could not merge " + candidate + + // " into " + head); + // } + } + } else { + if (head.isVip() && candidate.isVip()) { + logger.info( + "No intersection between {} and {}", + candidate, head); + } + } + } + } + } + } + + // Discard the merged filaments + removeMergedFilaments(); + } + + //-----------------------// + // removeMergedFilaments // + //-----------------------// + private void removeMergedFilaments () + { + for (Iterator it = filaments.iterator(); it.hasNext();) { + Glyph fil = it.next(); + + if (fil.getPartOf() != null) { + it.remove(); + } + } + } + + //~ Inner Classes ---------------------------------------------------------- + //-----------// + // Constants // + //-----------// + private static final class Constants + extends ConstantSet + { + //~ Instance fields ---------------------------------------------------- + + Constant.Double maxGapSlope = new Constant.Double( + "tangent", + 0.5, + "Maximum absolute slope for a gap"); + + Constant.Boolean printWatch = new Constant.Boolean( + false, + "Should we print out the stop watch?"); + + Constant.Ratio minSectionAspect = new Constant.Ratio( + 3, + "Minimum section aspect (length / thixkness)"); + + Constant.Ratio maxConsistentRatio = new Constant.Ratio( + 1.7, + "Maximum thickness ratio for consistent merge"); + + // + // Constants specified WRT mean line thickness + // ------------------------------------------- + // + Scale.LineFraction maxSectionThickness = new Scale.LineFraction( + 1.5, + "Maximum horizontal section thickness WRT mean line height"); + + Scale.LineFraction maxFilamentThickness = new Scale.LineFraction( + 1.5, + "Maximum filament thickness WRT mean line height"); + + Scale.LineFraction maxPosGap = new Scale.LineFraction( + 0.75, + "Maximum delta position for a gap between filaments"); + + // + // Constants specified WRT mean interline + // -------------------------------------- + // + Scale.Fraction minCoreSectionLength = new Scale.Fraction( + 1, + "Minimum length for a section to be considered as core"); + + Scale.Fraction maxOverlapDeltaPos = new Scale.Fraction( + 0.5, + "Maximum delta position between two overlapping filaments"); + + Scale.Fraction maxCoordGap = new Scale.Fraction( + 1, + "Maximum delta coordinate for a gap between filaments"); + + Scale.Fraction maxSpace = new Scale.Fraction( + 0.16, + "Maximum space between overlapping filaments"); + + Scale.Fraction maxExpansionSpace = new Scale.Fraction( + 0.02, + "Maximum space when expanding filaments"); + + Scale.Fraction maxPosGapForSlope = new Scale.Fraction( + 0.1, + "Maximum delta Y to check slope for a gap between filaments"); + + Scale.Fraction maxInvolvingLength = new Scale.Fraction( + 2, + "Maximum filament length to apply thickness test"); + + } + + //------------// + // Parameters // + //------------// + /** + * Class {@code Parameters} gathers all scale-dependent parameters. + */ + private class Parameters + { + //~ Instance fields ---------------------------------------------------- + + /** Probe width */ + public int probeWidth; + + /** Maximum acceptable thickness for sections */ + public int maxSectionThickness; + + /** Maximum acceptable thickness for filaments */ + public int maxFilamentThickness; + + /** Minimum acceptable length for core sections */ + public int minCoreSectionLength; + + /** Maximum acceptable delta position */ + public int maxOverlapDeltaPos; + + /** Maximum delta coordinate for real gap */ + public int maxCoordGap; + + /** Maximum delta position for real gaps */ + public int maxPosGap; + + /** Maximum space between overlapping filaments */ + public int maxSpace; + + /** Maximum space for expansion */ + public int maxExpansionSpace; + + /** Maximum filament length to apply thickness test */ + public int maxInvolvingLength; + + /** Maximum dy for slope check on real gap */ + public int maxPosGapForSlope; + + /** Minimum acceptable aspect for sections */ + public double minSectionAspect; + + /** Maximum slope for real gaps */ + public double maxGapSlope; + + //~ Methods ------------------------------------------------------------ + public void dump () + { + Main.dumping.dump(this); + } + + /** + * Initialize with default values + */ + public void initialize () + { + setMinCoreSectionLength(constants.minCoreSectionLength); + setMaxSectionThickness(constants.maxSectionThickness); + setMaxFilamentThickness(constants.maxFilamentThickness); + setMaxCoordGap(constants.maxCoordGap); + setMaxPosGap(constants.maxPosGap); + setMaxSpace(constants.maxSpace); + setMaxExpansionSpace(constants.maxExpansionSpace); + setMaxInvolvingLength(constants.maxInvolvingLength); + setMaxPosGapForSlope(constants.maxPosGapForSlope); + setMaxOverlapDeltaPos(constants.maxOverlapDeltaPos); + setMaxGapSlope(constants.maxGapSlope.getValue()); + setMinSectionAspect(constants.minSectionAspect.getValue()); + + probeWidth = scale.toPixels(BasicAlignment.getProbeWidth()); + + if (logger.isDebugEnabled()) { + dump(); + } + } + } +} diff --git a/src/main/omr/grid/GridBuilder.java b/src/main/omr/grid/GridBuilder.java new file mode 100644 index 0000000..340459a --- /dev/null +++ b/src/main/omr/grid/GridBuilder.java @@ -0,0 +1,266 @@ +//----------------------------------------------------------------------------// +// // +// G r i d B u i l d e r // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.grid; + +import omr.Main; + +import omr.constant.Constant; +import omr.constant.ConstantSet; + +import omr.glyph.Glyphs; +import omr.glyph.facets.Glyph; +import omr.glyph.ui.SymbolsEditor; + +import omr.run.RunsTable; + +import omr.sheet.Sheet; + +import omr.step.StepException; + +import omr.util.StopWatch; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Collection; +import java.util.Collections; + +/** + * Class {@code GridBuilder} computes the grid of systems of a sheet + * picture, based on the retrieval of horizontal staff lines and of + * vertical bar lines. + * + *

The actual processing is delegated to 3 companions:

    + *
  • {@link LinesRetriever} for retrieving all horizontal staff lines.
  • + *
  • {@link BarsRetriever} for retrieving main vertical bar lines.
  • + *
  • {@link TargetBuilder} for building the target grid.
  • + *
+ * + * @author Hervé Bitteur + */ +public class GridBuilder +{ + //~ Static fields/initializers --------------------------------------------- + + /** Specific application parameters */ + private static final Constants constants = new Constants(); + + /** Usual logger utility */ + private static final Logger logger = LoggerFactory.getLogger( + GridBuilder.class); + + //~ Instance fields -------------------------------------------------------- + /** Related sheet. */ + private final Sheet sheet; + + /** Companion in charge of staff lines. */ + private final LinesRetriever linesRetriever; + + /** Companion in charge of bar lines. */ + private final BarsRetriever barsRetriever; + + /** For runs display, if any. */ + private final RunsViewer runsViewer; + + //~ Constructors ----------------------------------------------------------- + //-------------// + // GridBuilder // + //-------------// + /** + * Retrieve the frames of all staff lines. + * + * @param sheet the sheet to process + */ + public GridBuilder (Sheet sheet) + { + this.sheet = sheet; + + barsRetriever = new BarsRetriever(sheet); + linesRetriever = new LinesRetriever(sheet, barsRetriever); + + runsViewer = (Main.getGui() != null) + ? new RunsViewer(sheet, linesRetriever, barsRetriever) : null; + } + + //~ Methods ---------------------------------------------------------------- + //-----------// + // buildInfo // + //-----------// + /** + * Compute and display the system frames of the sheet picture. + */ + public void buildInfo () + throws StepException + { + StopWatch watch = new StopWatch("GridBuilder"); + + try { + // Build the vertical and horizontal lags + watch.start("buildAllLags"); + buildAllLags(); + + // Display + if (Main.getGui() != null) { + displayEditor(); + } + + // Retrieve the horizontal staff lines filaments + watch.start("retrieveLines"); + linesRetriever.retrieveLines(); + + // Retrieve the major vertical barlines and thus the systems + watch.start("retrieveSystemBars"); + barsRetriever.retrieveSystemBars( + Collections.EMPTY_SET, + Collections.EMPTY_SET); + + // Complete the staff lines w/ short sections & filaments left over + watch.start("completeLines"); + linesRetriever.completeLines(); + + // Retrieve minor barlines (for measures) + barsRetriever.retrieveMeasureBars(); + + // Adjust ending points of all systems (side) bars + barsRetriever.adjustSystemBars(); + + /** Companion in charge of target grid */ + TargetBuilder targetBuilder = new TargetBuilder(sheet); + sheet.setTargetBuilder(targetBuilder); + + // Define the destination grid, if so desired + if (constants.buildDewarpedTarget.isSet()) { + watch.start("targetBuilder"); + targetBuilder.buildInfo(); + } + } catch (Throwable ex) { + logger.warn(sheet.getLogPrefix() + "Error in GridBuilder", ex); + } finally { + if (constants.printWatch.isSet()) { + watch.print(); + } + + if (Main.getGui() != null) { + sheet.getSymbolsEditor() + .refresh(); + } + } + } + + //------------// + // updateBars // + //------------// + /** + * Update the collection of bar candidates, removing the discarded + * ones and adding the new ones, and rebuild the barlines. + * + * @param oldSticks former glyphs to discard + * @param newSticks new glyphs to take as manual bar sticks + */ + public void updateBars (Collection oldSticks, + Collection newSticks) + { + logger.info("updateBars"); + logger.info("Old {}", Glyphs.toString(oldSticks)); + logger.info("New {}", Glyphs.toString(newSticks)); + + try { + barsRetriever.retrieveSystemBars(oldSticks, newSticks); + } catch (Exception ex) { + logger.warn("updateBars. retrieveSystemBars", ex); + } + + barsRetriever.retrieveMeasureBars(); + barsRetriever.adjustSystemBars(); + } + + //--------------// + // buildAllLags // + //--------------// + /** + * From the sheet picture, build the vertical lag (for bar lines) + * and the horizontal lag (for staff lines). + */ + private void buildAllLags () + { + final boolean showRuns = constants.showRuns.isSet() + && (Main.getGui() != null); + final StopWatch watch = new StopWatch("buildAllLags"); + + try { + // We already have all foreground pixels as vertical runs + RunsTable wholeVertTable = sheet.getWholeVerticalTable(); + + // Note: from that point on, we could simply discard the sheet picture + // and save memory, since wholeVertTable contains all foreground pixels. + // For the time being, it is kept alive for display purpose, and to + // allow the dewarping of the initial picture. + + // View on the initial runs (just for information) + if (showRuns) { + runsViewer.display(wholeVertTable); + } + + // hLag creation + watch.start("linesRetriever.buildLag"); + + RunsTable longVertTable = linesRetriever.buildLag( + wholeVertTable, + showRuns); + + // vLag creation + watch.start("barsRetriever.buildLag"); + barsRetriever.buildLag(longVertTable); + } finally { + if (constants.printWatch.isSet()) { + watch.print(); + } + } + } + + //---------------// + // displayEditor // + //---------------// + private void displayEditor () + { + sheet.createSymbolsControllerAndEditor(); + + SymbolsEditor editor = sheet.getSymbolsEditor(); + + // Specific rendering for grid + editor.addItemRenderer(linesRetriever); + editor.addItemRenderer(barsRetriever); + } + + //~ Inner Classes ---------------------------------------------------------- + //-----------// + // Constants // + //-----------// + private static final class Constants + extends ConstantSet + { + //~ Instance fields ---------------------------------------------------- + + Constant.Boolean showRuns = new Constant.Boolean( + false, + "Should we show view on runs?"); + + Constant.Boolean printWatch = new Constant.Boolean( + false, + "Should we print out the stop watch?"); + + Constant.Boolean buildDewarpedTarget = new Constant.Boolean( + false, + "Should we build a dewarped target?"); + + } +} diff --git a/src/main/omr/grid/IntersectionSequence.java b/src/main/omr/grid/IntersectionSequence.java new file mode 100644 index 0000000..69a6076 --- /dev/null +++ b/src/main/omr/grid/IntersectionSequence.java @@ -0,0 +1,95 @@ +//----------------------------------------------------------------------------// +// // +// I n t e r s e c t i o n S e q u e n c e // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.grid; + +import omr.glyph.facets.Glyph; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Comparator; +import java.util.Iterator; +import java.util.List; +import java.util.TreeSet; + +/** + * Class {@code IntersectionSequence} handles a sorted sequence of + * sticks intersections. + * + * @author Hervé Bitteur + */ +class IntersectionSequence + extends TreeSet +{ + //~ Static fields/initializers --------------------------------------------- + + /** Usual logger utility */ + private static final Logger logger = LoggerFactory.getLogger( + IntersectionSequence.class); + + //~ Constructors ----------------------------------------------------------- + //----------------------// + // IntersectionSequence // + //----------------------// + /** + * Creates a new IntersectionSequence object. + * + * @param comparator the comparator (hori or vert) to use for the sequence + */ + public IntersectionSequence ( + Comparator comparator) + { + super(comparator); + } + + //~ Methods ---------------------------------------------------------------- + //-----------// + // getSticks // + //-----------// + public List getSticks () + { + return StickIntersection.sticksOf(this); + } + + //--------// + // reduce // + //--------// + public void reduce (double maxDeltaPos) + { + // If 2 sticks are close in position, simply merge them + for (Iterator headIt = iterator(); headIt.hasNext();) { + StickIntersection head = headIt.next(); + + for (StickIntersection tail : tailSet(head, false)) { + if (tail.getStickAncestor() == head.getStickAncestor()) { + continue; + } + + if ((tail.x - head.x) <= maxDeltaPos) { + if (logger.isDebugEnabled() + || head.getStickAncestor() + .isVip() + || tail.getStickAncestor() + .isVip()) { + logger.info("Merging verticals {} & {}", head, tail); + } + + Glyph tailAncestor = tail.getStickAncestor(); + tailAncestor.stealSections(head.getStickAncestor()); + headIt.remove(); + + break; + } + } + } + } +} diff --git a/src/main/omr/grid/LagWeaver.java b/src/main/omr/grid/LagWeaver.java new file mode 100644 index 0000000..21713de --- /dev/null +++ b/src/main/omr/grid/LagWeaver.java @@ -0,0 +1,685 @@ +//----------------------------------------------------------------------------// +// // +// L a g W e a v e r // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.grid; + +import omr.glyph.GlyphsBuilder; +import omr.glyph.Shape; +import omr.glyph.facets.Glyph; + +import omr.lag.Lag; +import omr.lag.Section; +import omr.lag.Sections; + +import omr.run.Orientation; +import omr.run.Run; + +import omr.sheet.Sheet; + +import omr.util.Predicate; +import omr.util.StopWatch; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.awt.Point; +import java.awt.geom.PathIterator; +import static java.awt.geom.PathIterator.*; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.ListIterator; + +/** + * Class {@code LagWeaver} is just a prototype. TODO. + * + * @author Hervé Bitteur + */ +public class LagWeaver +{ + //~ Static fields/initializers --------------------------------------------- + + /** Usual logger utility */ + private static final Logger logger = LoggerFactory.getLogger(LagWeaver.class); + + /** Table dx/dy -> Heading */ + private static final Heading[][] headings = { + {null, Heading.NORTH, null}, + { + Heading.WEST, null, + Heading.EAST + }, + {null, Heading.SOUTH, null} + }; + + //~ Enumerations ----------------------------------------------------------- + private static enum Heading + { + //~ Enumeration constant initializers ---------------------------------- + + NORTH, + EAST, + SOUTH, + WEST; + + //~ Methods ------------------------------------------------------------ + public boolean insideCornerTo (Heading next) + { + switch (this) { + case NORTH: + return next == WEST; + + case EAST: + return next == NORTH; + + case SOUTH: + return next == EAST; + + case WEST: + return next == SOUTH; + } + + return false; // Unreachable stmt + } + } + + //~ Instance fields -------------------------------------------------------- + /** Related sheet */ + private final Sheet sheet; + + /** Vertical lag */ + private final Lag vLag; + + /** Horizontal lag */ + private final Lag hLag; + + /** + * Actual points around current vLag section to check to hLag presence + * (relevant only during horiWithVert) + */ + private final List pointsAside = new ArrayList<>(); + + /** Points to check for source sections above in hLag */ + private final List pointsAbove = new ArrayList<>(); + + /** Points to check for target sections below in hLag */ + private final List pointsBelow = new ArrayList<>(); + + //~ Constructors ----------------------------------------------------------- + //-----------// + // LagWeaver // + //-----------// + /** + * Creates a new LagWeaver object. + * + * @param sheet the related sheet, which holds the v & h lags + */ + public LagWeaver (Sheet sheet) + { + this.sheet = sheet; + + vLag = sheet.getVerticalLag(); + hLag = sheet.getHorizontalLag(); + } + + //~ Methods ---------------------------------------------------------------- + //-----------// + // buildInfo // + //-----------// + public void buildInfo () + { + StopWatch watch = new StopWatch("LagWeaver"); + + // Remove staff line stuff from hLag + watch.start("purge hLag"); + + List
staffLinesSections = removeStaffLines(hLag); + + logger.debug("{}StaffLine sections removed: {}", + sheet.getLogPrefix(), staffLinesSections.size()); + + watch.start("Hori <-> Hori"); + horiWithHori(); + + watch.start("Hori <-> Vert"); + horiWithVert(); + + watch.start("buildGlyphs"); + buildGlyphs(); + + // The end + ///watch.print(); + } + + //---------------// + // addPointAbove // + //---------------// + private void addPointAbove (int x, + int y) + { + logger.debug("addPointAbove {},{}", x, y); + pointsAbove.add(new Point(x, y)); + } + + //---------------// + // addPointAside // + //---------------// + private void addPointAside (int x, + int y) + { + //logger.debug("addPointAside " + x + "," + y); + pointsAside.add(new Point(x, y)); + } + + //---------------// + // addPointBelow // + //---------------// + private void addPointBelow (int x, + int y) + { + logger.debug("addPointBelow {},{}", x, y); + pointsBelow.add(new Point(x, y)); + } + + //-------------// + // buildGlyphs // + //-------------// + private void buildGlyphs () + { + // Group (unknown) sections into glyphs + // Consider all unknown vertical & horizontal sections + List
allSections = new ArrayList<>(); + + for (Section section : sheet.getVerticalLag().getSections()) { + if (!section.isKnown()) { + section.setProcessed(false); + allSections.add(section); + } else { + section.setProcessed(true); + } + } + + for (Section section : sheet.getHorizontalLag().getSections()) { + if (!section.isKnown()) { + section.setProcessed(false); + allSections.add(section); + } else { + section.setProcessed(true); + } + } + + GlyphsBuilder.retrieveGlyphs( + allSections, + sheet.getNest(), + sheet.getScale()); + } + + //------------------// + // checkPointsAbove // + //------------------// + private void checkPointsAbove (Section lSect) + { + boolean added = false; + + for (Point pt : pointsAbove) { + Run run = hLag.getRunAt(pt.x, pt.y); + + if (run != null) { + Section hSect = run.getSection(); + + if (hSect != null) { + hSect.addTarget(lSect); + added = true; + } + } + } + + if (added && logger.isDebugEnabled()) { + logger.info("lSect#{} checks:{}{}", + lSect.getId(), + pointsAbove.size(), + Sections.toString(" sources", lSect.getSources())); + } + } + + //------------------// + // checkPointsAside // + //------------------// + private void checkPointsAside (Section vSect) + { + boolean added = false; + + for (Point pt : pointsAside) { + Run run = hLag.getRunAt(pt.x, pt.y); + + if (run != null) { + Section hSect = run.getSection(); + + if (hSect != null) { + vSect.addOppositeSection(hSect); + hSect.addOppositeSection(vSect); + added = true; + } + } + } + + if (added && logger.isDebugEnabled()) { + logger.info("vSect#{} checks:{}{}", + vSect.getId(), + pointsAside.size(), + Sections.toString(" hSects", vSect.getOppositeSections())); + } + } + + //------------------// + // checkPointsBelow // + //------------------// + private void checkPointsBelow (Section lSect) + { + boolean added = false; + + for (Point pt : pointsBelow) { + Run run = hLag.getRunAt(pt.x, pt.y); + + if (run != null) { + Section hSect = run.getSection(); + + if (hSect != null) { + lSect.addTarget(hSect); + added = true; + } + } + } + + if (added && logger.isDebugEnabled()) { + logger.info("lSect#{} checks:{}{}", lSect.getId(), + pointsBelow.size(), + Sections.toString(" targets", lSect.getTargets())); + } + } + + //------------// + // getHeading // + //------------// + private Heading getHeading (Point prevPt, + Point pt) + { + int dx = Integer.signum(pt.x - prevPt.x); + int dy = Integer.signum(pt.y - prevPt.y); + + return headings[1 + dy][1 + dx]; + } + + //--------------// + // horiWithHori // + //--------------// + /** + * Connect, when appropriate, the long horizontal sections (built from long + * runs) with short horizontal sections (built later from shorter runs). + * Without such connections, glyph building would suffer over-segmentation. + * + *

We take each long section in turn and check for connection, above and + * below, with short sections. If positive, we cross-connect them. + */ + private void horiWithHori () + { + int maxLongId = sheet.getLongSectionMaxId(); + + // Process each long section in turn + for (Section lSect : hLag.getSections()) { + if (lSect.getId() > maxLongId) { + continue; + } + + final int sectTop = lSect.getFirstPos(); + final int sectLeft = lSect.getStartCoord(); + final int sectBottom = lSect.getLastPos(); + final double[] coords = new double[2]; + final boolean[] occupied = new boolean[lSect.getLength( + Orientation.HORIZONTAL)]; + Point prevPt = null; + Point pt; + Heading prevHeading = null; + Heading heading = null; + pointsAbove.clear(); + pointsBelow.clear(); + + for (PathIterator it = lSect.getPathIterator(); !it.isDone();) { + int kind = it.currentSegment(coords); + pt = new Point((int) coords[0], (int) coords[1]); + + if (kind == SEG_LINETO) { + heading = getHeading(prevPt, pt); + logger.debug("{} {} {}", prevPt, heading, pt); + + switch (heading) { + case NORTH: + + // No pixel on right + if (prevHeading == Heading.WEST) { + removePointAbove(prevPt.x, prevPt.y - 1); + } + + break; + + case WEST: { + int dir = -1; + + // Check pixels on row above + Arrays.fill(occupied, false); + + int y = pt.y - 1; + int xStart = prevPt.x - 1; + + if (prevHeading == Heading.SOUTH) { + xStart += dir; + } + + // Special case for first run, check adjacent section + if (pt.y == sectTop) { + for (Section adj : lSect.getSources()) { + Run run = adj.getLastRun(); + int left = Math.max(run.getStart() - 1, pt.x); + int right = Math.min(run.getStop() + 1, xStart); + + for (int x = left; x <= right; x++) { + occupied[x - sectLeft] = true; + } + } + } + + int xBreak = pt.x - 1; + + for (int x = xStart; x != xBreak; x += dir) { + if (!occupied[x - sectLeft]) { + addPointAbove(x, y); + } + } + + break; + } + + case SOUTH: + + // No pixel on left + if (prevHeading == Heading.EAST) { + removePointBelow(prevPt.x - 1, prevPt.y); + } + + break; + + case EAST: { + int dir = +1; + + // Check pixels on row below + Arrays.fill(occupied, false); + + int y = pt.y; + int xStart = prevPt.x; + + if (prevHeading == Heading.NORTH) { + xStart += dir; + } + + int xBreak = pt.x; + + // Special case for last run, check adjacent section + if ((pt.y - 1) == sectBottom) { + for (Section adj : lSect.getTargets()) { + Run run = adj.getFirstRun(); + int left = Math.max(run.getStart() - 1, xStart); + int right = Math.min( + run.getStop() + 1, + xBreak - 1); + + for (int x = left; x <= right; x++) { + occupied[x - sectLeft] = true; + } + } + } + + for (int x = xStart; x != xBreak; x += dir) { + if (!occupied[x - sectLeft]) { + addPointBelow(x, y); + } + } + + break; + } + } + } + + prevHeading = heading; + prevPt = pt; + it.next(); + } + + checkPointsAbove(lSect); + checkPointsBelow(lSect); + } + } + + //--------------// + // horiWithVert // + //--------------// + private void horiWithVert () + { + // Process each vertical section in turn + for (Section vSect : vLag.getSections()) { + final int sectTop = vSect.getStartCoord(); + final int sectLeft = vSect.getFirstPos(); + final int sectRight = vSect.getLastPos(); + final double[] coords = new double[2]; + final boolean[] occupied = new boolean[vSect.getLength( + Orientation.VERTICAL)]; + Point prevPt = null; + Point pt = null; + Heading prevHeading = null; + Heading heading = null; + pointsAside.clear(); + + for (PathIterator it = vSect.getPathIterator(); !it.isDone();) { + int kind = it.currentSegment(coords); + pt = new Point((int) coords[0], (int) coords[1]); + + if (kind == SEG_LINETO) { + heading = getHeading(prevPt, pt); + + //logger.info(prevPt + " " + heading + " " + pt); + switch (heading) { + case NORTH: { + int dir = -1; + // Check pixels on left column + Arrays.fill(occupied, false); + + int x = pt.x - 1; + int yStart = prevPt.y - 1; + + if (prevHeading == Heading.EAST) { + yStart += dir; + } + + // Special case for section left run + if (pt.x == sectLeft) { + for (Section adj : vSect.getSources()) { + Run run = adj.getLastRun(); + int top = Math.max(run.getStart() - 1, pt.y); + int bot = Math.min(run.getStop() + 1, yStart); + + for (int y = top; y <= bot; y++) { + occupied[y - sectTop] = true; + } + } + } + + int yBreak = pt.y - 1; + + for (int y = yStart; y != yBreak; y += dir) { + if (!occupied[y - sectTop]) { + addPointAside(x, y); + } + } + } + + break; + + case WEST: + + // No pixel above + if (prevHeading == Heading.NORTH) { + removePointAside(prevPt.x - 1, prevPt.y); + } + + break; + + case SOUTH: { + int dir = +1; + // Check pixels on right column + Arrays.fill(occupied, false); + + int x = pt.x; + int yStart = prevPt.y; + + if (prevHeading == Heading.WEST) { + yStart += dir; + } + + int yBreak = pt.y; + + // Special case for section right run + if ((pt.x - 1) == sectRight) { + for (Section adj : vSect.getTargets()) { + Run run = adj.getFirstRun(); + int top = Math.max(run.getStart() - 1, yStart); + int bot = Math.min( + run.getStop() + 1, + yBreak - 1); + + for (int y = top; y <= bot; y++) { + occupied[y - sectTop] = true; + } + } + } + + for (int y = yStart; y != yBreak; y += dir) { + if (!occupied[y - sectTop]) { + addPointAside(x, y); + } + } + } + + break; + + case EAST: + + // No pixel below + if (prevHeading == Heading.SOUTH) { + removePointAside(prevPt.x, prevPt.y - 1); + } + + break; + } + } + + prevHeading = heading; + prevPt = pt; + it.next(); + } + + checkPointsAside(vSect); + } + } + + //-------------// + // removePoint // + //-------------// + private void removePoint (List points, + int x, + int y) + { + if (!points.isEmpty()) { + ListIterator iter = points.listIterator(points.size()); + Point lastCorner = iter.previous(); + + if ((lastCorner.x == x) && (lastCorner.y == y)) { + iter.remove(); + } + } + } + + //------------------// + // removePointAbove // + //------------------// + private void removePointAbove (int x, + int y) + { + logger.debug("Removing corner above x:{} y:{}", x, y); + removePoint(pointsAbove, x, y); + } + + //------------------// + // removePointAside // + //------------------// + private void removePointAside (int x, + int y) + { + removePoint(pointsAside, x, y); + } + + //------------------// + // removePointBelow // + //------------------// + private void removePointBelow (int x, + int y) + { + logger.debug("Removing corner below x:{} y:{}", x, y); + removePoint(pointsBelow, x, y); + } + + //------------------// + // removeStaffLines // + //------------------// + private List

removeStaffLines (Lag hLag) + { + return hLag.purgeSections( + new Predicate
() + { + @Override + public boolean check (Section section) + { + Glyph glyph = section.getGlyph(); + + if ((glyph != null) + && (glyph.getShape() == Shape.STAFF_LINE)) { + /** + * Narrow horizontal section can be kept to avoid + * over-segmentation between vertical sections + */ + if ((section.getLength(Orientation.HORIZONTAL) == 1) + && (section.getLength(Orientation.VERTICAL) > 1)) { + if (section.isVip() || logger.isDebugEnabled()) { + logger.info("Keeping staffline section {}", + section); + } + + section.setGlyph(null); + + return false; + } else { + return true; + } + } else { + return false; + } + } + }); + } +} diff --git a/src/main/omr/grid/LineCluster.java b/src/main/omr/grid/LineCluster.java new file mode 100644 index 0000000..bc43e5c --- /dev/null +++ b/src/main/omr/grid/LineCluster.java @@ -0,0 +1,756 @@ +//----------------------------------------------------------------------------// +// // +// L i n e C l u s t e r // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.grid; + +import omr.glyph.Glyphs; + +import omr.lag.Section; + +import omr.run.Orientation; + +import omr.util.GeoUtil; +import omr.util.Vip; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.awt.Graphics2D; +import java.awt.Point; +import java.awt.Rectangle; +import java.awt.geom.Point2D; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Comparator; +import java.util.List; +import java.util.Map.Entry; +import java.util.SortedMap; +import java.util.TreeMap; + +/** + * Class {@code LineCluster} is meant to aggregate instances of + * {@link Filament} that are linked by {@link FilamentComb} instances + * and thus a cluster represents a staff candidate. + * + * @author Hervé Bitteur + */ +public class LineCluster + implements Vip +{ + //~ Static fields/initializers --------------------------------------------- + + /** Usual logger utility */ + private static final Logger logger = LoggerFactory.getLogger(LineCluster.class); + + /** For comparing LineCluster instances on their true length */ + public static final Comparator reverseLengthComparator = new Comparator() + { + @Override + public int compare (LineCluster c1, + LineCluster c2) + { + // Sort on reverse length + return Double.compare(c2.getTrueLength(), c1.getTrueLength()); + } + }; + + //~ Instance fields -------------------------------------------------------- + /** Id for debug */ + private final int id; + + /** Interline for this cluster */ + private final int interline; + + /** Reference to cluster this one has been included into, if any */ + private LineCluster parent; + + /** Composing lines, ordered by their relative position (ordinate) */ + private SortedMap lines; + + /** (Cached) bounding box of this cluster */ + private Rectangle contourBox; + + /** CLuster true length */ + private Integer trueLength; + + /** For debugging */ + private boolean vip = false; + + //~ Constructors ----------------------------------------------------------- + //-------------// + // LineCluster // + //-------------// + /** + * Creates a new LineCluster object. + * + * @param seed the first filament of the cluster + */ + public LineCluster (int interline, + LineFilament seed) + { + if (logger.isDebugEnabled() || seed.isVip()) { + logger.info("Creating cluster with F{}", seed.getId()); + + if (seed.isVip()) { + setVip(); + } + } + + this.interline = interline; + this.id = seed.getId(); + + lines = new TreeMap<>(); + + include(seed, 0); + } + + //~ Methods ---------------------------------------------------------------- + //---------// + // destroy // + //---------// + /** + * Remove the link back from filaments to this cluster. + */ + public void destroy () + { + for (FilamentLine line : lines.values()) { + line.fil.setCluster(null, 0); + line.fil.getCombs().clear(); + } + } + + //-------------// + // getAncestor // + //-------------// + /** + * Report the top ancestor of this cluster. + * + * @return the cluster ancestor + */ + public LineCluster getAncestor () + { + LineCluster cluster = this; + + while (cluster.parent != null) { + cluster = cluster.parent; + } + + return cluster; + } + + //-----------// + // getBounds // + //-----------// + public Rectangle getBounds () + { + if (contourBox == null) { + Rectangle box = null; + + for (FilamentLine line : getLines()) { + if (box == null) { + box = new Rectangle(line.getBounds()); + } else { + box.add(line.getBounds()); + } + } + + contourBox = box; + } + + if (contourBox != null) { + return new Rectangle(contourBox); + } else { + return null; + } + } + + //-----------// + // getCenter // + //-----------// + /** + * Report the center of cluster. + * + * @return the center + */ + public Point getCenter () + { + Rectangle box = getBounds(); + + return new Point( + box.x + (box.width / 2), + box.y + (box.height / 2)); + } + + //--------------// + // getFirstLine // + //--------------// + public FilamentLine getFirstLine () + { + return lines.get(lines.firstKey()); + } + + //-------// + // getId // + //-------// + /** + * @return the id + */ + public int getId () + { + return id; + } + + //--------------// + // getInterline // + //--------------// + /** + * @return the interline + */ + public int getInterline () + { + return interline; + } + + //-------------// + // getLastLine // + //-------------// + public FilamentLine getLastLine () + { + return lines.get(lines.lastKey()); + } + + //----------// + // getLines // + //----------// + public Collection getLines () + { + return lines.values(); + } + + //-----------// + // getParent // + //-----------// + /** + * @return the parent + */ + public LineCluster getParent () + { + return parent; + } + + //-------------// + // getPointsAt // + //-------------// + /** + * Report the sequence of points that correspond to a provided + * abscissa. + * + * @param x the provided abscissa + * @param xMargin maximum abscissa margin for horizontal extrapolation + * @param interline the standard interline value, used for vertical + * extrapolations + * @return the sequence of cluster points, from top to bottom, with perhaps + * some holes indicated by null values + */ + public List getPointsAt (double x, + int xMargin, + int interline, + double globalSlope) + { + SortedMap points = new TreeMap<>(); + List holes = new ArrayList<>(); + + for (Entry entry : lines.entrySet()) { + int pos = entry.getKey(); + FilamentLine line = entry.getValue(); + + if (line.isWithinRange(x)) { + points.put( + pos, + new Point2D.Double(x, line.yAt(x))); + } else { + holes.add(pos); + } + } + + // Interpolate or extrapolate the missing values if any + for (int pos : holes) { + Integer prevPos = null; + Double prevVal = null; + + for (int p = pos - 1; p >= lines.firstKey(); p--) { + Point2D pt = points.get(p); + + if (pt != null) { + prevPos = p; + prevVal = pt.getY(); + + break; + } + } + + Integer nextPos = null; + Double nextVal = null; + + for (int p = pos + 1; p <= lines.lastKey(); p++) { + Point2D pt = points.get(p); + + if (pt != null) { + nextPos = p; + nextVal = pt.getY(); + + break; + } + } + + Double y = null; + + // Interpolate vertically + if ((prevPos != null) && (nextPos != null)) { + y = prevVal + + (((pos - prevPos) * (nextVal - prevVal)) / (nextPos + - prevPos)); + } else { + // Extrapolate vertically, only for one interline max + if ((prevPos != null) && ((pos - prevPos) == 1)) { + y = prevVal + interline; + } else if ((nextPos != null) && ((nextPos - pos) == 1)) { + y = nextVal - interline; + } else { + // Extrapolate horizontally on a short distance + FilamentLine line = lines.get(pos); + Point2D point = (x <= line.getStartPoint().getX()) + ? line.getStartPoint() + : line.getStopPoint(); + double dx = x - point.getX(); + + if (Math.abs(dx) <= xMargin) { + y = point.getY() + (dx * globalSlope); + } + } + } + + points.put(pos, (y != null) ? new Point2D.Double(x, y) : null); + } + + return new ArrayList<>(points.values()); + } + + //---------// + // getSize // + //---------// + public int getSize () + { + return lines.size(); + } + + //-----------// + // getStarts // + //-----------// + public List getStarts () + { + List points = new ArrayList<>(getSize()); + + for (FilamentLine line : lines.values()) { + points.add(line.getStartPoint()); + } + + return points; + } + + //----------// + // getStops // + //----------// + public List getStops () + { + List points = new ArrayList<>(getSize()); + + for (FilamentLine line : lines.values()) { + points.add(line.getStopPoint()); + } + + return points; + } + + //---------------// + // getTrueLength // + //---------------// + /** + * Report a measurement of the cluster length. + * + * @return the mean true length of cluster lines + */ + public int getTrueLength () + { + if (trueLength == null) { + // Determine mean true line length in this cluster + int meanTrueLength = 0; + + for (FilamentLine line : lines.values()) { + meanTrueLength += line.fil.trueLength(); + } + + meanTrueLength /= lines.size(); + logger.debug("TrueLength: {} for {}", meanTrueLength, this); + + trueLength = meanTrueLength; + } + + return trueLength; + } + + //------------------------// + // includeFilamentByIndex // + //------------------------// + /** + * Include a filament to this cluster, using the provided relative + * line index counted from zero (rather than the line position). + * Check this room is "free" on the cluster line + * + * @param filament the filament to include + * @param index the zero-based line index + * @return true if there was room for inclusion + */ + public boolean includeFilamentByIndex (LineFilament filament, + int index) + { + final Rectangle filBox = filament.getBounds(); + int i = 0; + + for (Entry entry : lines.entrySet()) { + if (i++ == index) { + FilamentLine line = entry.getValue(); + + // Check for horizontal room + // For filaments one above the other, check resulting thickness + for (Section section : line.fil.getMembers()) { + // Horizontal overlap? + Rectangle sctBox = section.getBounds(); + int overlap = GeoUtil.xOverlap(filBox, sctBox); + if (overlap > 0) { + // Check resulting thickness + double thickness = Glyphs.getThicknessAt( + Math.max(filBox.x, sctBox.x) + overlap / 2, + Orientation.HORIZONTAL, + filament, + line.fil); + + if (thickness > line.fil.getScale().getMaxFore()) { + if (filament.isVip() || logger.isDebugEnabled()) { + logger.info("No room for {} in {}", + filament, this); + } + + return false; + } + } + } + + line.add(filament); + filament.setCluster(this, entry.getKey()); + invalidateCache(); + + return true; + } + } + + return false; // Should not happen + } + + //-------// + // isVip // + //-------// + @Override + public boolean isVip () + { + return vip; + } + + //-----------// + // mergeWith // + //-----------// + public void mergeWith (LineCluster that, + int deltaPos) + { + include( + that, + deltaPos + (this.lines.firstKey() - that.lines.firstKey())); + } + + //--------// + // render // + //--------// + public void render (Graphics2D g) + { + for (FilamentLine line : lines.values()) { + line.render(g); + } + } + + //---------------// + // renumberLines // + //---------------// + /** + * Renumber the remaining lines counting from zero. + */ + public void renumberLines () + { + // Renumbering + int firstPos = lines.firstKey(); + + if (firstPos != 0) { + SortedMap newLines = new TreeMap<>(); + + for (Entry entry : lines.entrySet()) { + int pos = entry.getKey(); + int newPos = pos - firstPos; + FilamentLine line = entry.getValue(); + line.fil.setCluster(this, newPos); + newLines.put(newPos, new FilamentLine(line.fil)); + } + + lines = newLines; + } + + invalidateCache(); + } + + // //-----------// + // // Constants // + // //-----------// + // private static final class Constants + // extends ConstantSet + // { + // //~ Instance fields ---------------------------------------------------- + // + // final Constant.Ratio minTrueLength = new Constant.Ratio( + // 0.4, + // "Minimum true length ratio to keep a line in a cluster"); + // } + //--------// + // setVip // + //--------// + @Override + public void setVip () + { + vip = true; + } + + //----------// + // toString // + //----------// + @Override + public String toString () + { + StringBuilder sb = new StringBuilder("{Cluster#"); + sb.append(getId()); + + sb.append(" interline:").append(getInterline()); + + sb.append(" size:").append(getSize()); + + for (Entry entry : lines.entrySet()) { + sb.append(" ").append(entry.getValue()); + } + + sb.append("}"); + + return sb.toString(); + } + + //------// + // trim // + //------// + /** + * Remove lines in excess. + * + * @param count the target line count + */ + public void trim (int count) + { + logger.debug("Trim {}", this); + + // // Determine max true line length in this cluster + // int maxTrueLength = 0; + // + // for (FilamentLine line : lines.values()) { + // maxTrueLength = Math.max(maxTrueLength, line.fil.trueLength()); + // } + // + // int minTrueLength = (int) Math.rint( + // maxTrueLength * constants.minTrueLength.getValue()); + // + // // Pruning + // for (Iterator it = lines.keySet() + // .iterator(); it.hasNext();) { + // Integer key = it.next(); + // FilamentLine line = lines.get(key); + // + // if (line.fil.trueLength() < minTrueLength) { + // it.remove(); + // line.fil.setCluster(null, 0); + // line.fil.getCombs() + // .clear(); + // } + // } + + // Pruning + while (lines.size() > count) { + // Remove the top or bottom line + FilamentLine top = lines.get(lines.firstKey()); + int topWL = top.fil.trueLength(); + FilamentLine bot = lines.get(lines.lastKey()); + int botWL = bot.fil.trueLength(); + FilamentLine line = null; + + if (topWL < botWL) { + line = top; + lines.remove(lines.firstKey()); + } else { + line = bot; + lines.remove(lines.lastKey()); + } + + // House keeping + line.fil.setCluster(null, 0); + line.fil.getCombs().clear(); + } + + renumberLines(); + invalidateCache(); + } + + //---------// + // getLine // + //---------// + private FilamentLine getLine (int pos, + LineFilament fil) + { + FilamentLine line = lines.get(pos); + + if (line == null) { + line = new FilamentLine(fil); + lines.put(pos, line); + } + + return line; + } + + //---------// + // include // + //---------// + /** + * Include a filament, with all its combs. + * + * @param pivot the filament to include + * @param pivotPos the imposed position within the cluster + */ + private void include (LineFilament pivot, + int pivotPos) + { + if (logger.isDebugEnabled() || pivot.isVip()) { + logger.info("{} include pivot:{} at pos:{}", + this, pivot.getId(), pivotPos); + + if (pivot.isVip()) { + setVip(); + } + } + + LineFilament ancestor = (LineFilament) pivot.getAncestor(); + + // Loop on all combs that involve this filament + for (FilamentComb comb : pivot.getCombs().values()) { + if (comb.isProcessed()) { + continue; + } + + comb.setProcessed(true); + + int deltaPos = pivotPos - comb.getIndex(pivot); + logger.debug("{} deltaPos:{}", comb, deltaPos); + + // Dispatch content of comb to proper lines + for (int i = 0; i < comb.getCount(); i++) { + LineFilament fil = (LineFilament) comb.getFilament(i). + getAncestor(); + LineCluster cluster = fil.getCluster(); + + if (cluster == null) { + int pos = i + deltaPos; + FilamentLine line = getLine(pos, null); + line.add(fil); + + if (fil.isVip()) { + logger.info("Adding {} to {} at pos {}", + fil, this, pos); + setVip(); + } + + fil.setCluster(this, pos); + + if (fil != ancestor) { + include(fil, pos); // Recursively + } + } else if (cluster.getAncestor() != this.getAncestor()) { + // Need to merge the two clusters + include(cluster, (i + deltaPos) - fil.getClusterPos()); + } + } + } + } + + //---------// + // include // + //---------// + /** + * Merge another cluster with this one. + * + * @param that the other cluster + * @param deltaPos the delta to apply to that cluster positions + */ + private void include (LineCluster that, + int deltaPos) + { + if (logger.isDebugEnabled() || isVip() || that.isVip()) { + logger.info("Inclusion of {} into {} deltaPos:{}", + that, this, deltaPos); + + if (that.isVip()) { + setVip(); + } + } + + for (Entry entry : that.lines.entrySet()) { + int pos = entry.getKey() + deltaPos; + FilamentLine line = entry.getValue(); + getLine(pos, null).include(line); + } + + that.parent = this; + + if (logger.isDebugEnabled()) { + logger.debug("Merged:{}", that); + logger.debug("Merger:{}", this); + } + + invalidateCache(); + } + + //-----------------// + // invalidateCache // + //-----------------// + private void invalidateCache () + { + contourBox = null; + trueLength = null; + } +} diff --git a/src/main/omr/grid/LineFilament.java b/src/main/omr/grid/LineFilament.java new file mode 100644 index 0000000..bf56c37 --- /dev/null +++ b/src/main/omr/grid/LineFilament.java @@ -0,0 +1,214 @@ +//----------------------------------------------------------------------------// +// // +// L i n e F i l a m e n t // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.grid; + +import omr.sheet.Scale; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.List; +import java.util.SortedMap; +import java.util.TreeMap; + +/** + * Class {@code LineFilament} is a {@link Filament}, used as (part of) + * a candidate staff line. + */ +public class LineFilament + extends Filament +{ + //~ Static fields/initializers --------------------------------------------- + + /** Usual logger utility */ + private static final Logger logger = LoggerFactory.getLogger(LineFilament.class); + + //~ Instance fields -------------------------------------------------------- + // + /** Combs where this filament appears. map (column index -> comb) */ + private SortedMap combs; + + /** The line cluster this filament is part of, if any. */ + private LineCluster cluster; + + /** + * Relative position in cluster. + * (relevant only if cluster is not null) + */ + private int clusterPos; + + //~ Constructors ----------------------------------------------------------- + // + //--------------// + // LineFilament // + //--------------// + /** + * Creates a new LineFilament object. + * + * @param scale scaling data + */ + public LineFilament (Scale scale) + { + super(scale, LineFilamentAlignment.class); + } + + //~ Methods ---------------------------------------------------------------- + // + //---------// + // addComb // + //---------// + /** + * Add a comb where this filament appears + * + * @param column the sheet column index of the comb + * @param comb the comb which contains this filament + */ + public void addComb (int column, + FilamentComb comb) + { + if (combs == null) { + combs = new TreeMap<>(); + } + + combs.put(column, comb); + } + + //--------// + // dumpOf // + //--------// + @Override + public String dumpOf () + { + StringBuilder sb = new StringBuilder(); + + sb.append(super.dumpOf()); + sb.append(String.format(" cluster=%s%n", cluster)); + sb.append(String.format(" clusterPos=%s%n", clusterPos)); + sb.append(String.format(" combs=%s%n", combs)); + + return sb.toString(); + } + + //-----------// + // fillHoles // + //-----------// + /** + * Fill large holes (due to missing intermediate points) in this + * filament, by interpolating (or extrapolating) from the + * collection of rather parallel fils, this filament is part of + * (at provided clusterPos). + * + * @param fils the provided collection of parallel filaments + */ + public void fillHoles (List fils) + { + getAlignment().fillHoles(clusterPos, fils); + } + + //------------// + // getCluster // + //------------// + /** + * Report the line cluster, if any, this filament is part of + * + * @return the containing cluster, or null + */ + public LineCluster getCluster () + { + return cluster; + } + + //---------------// + // getClusterPos // + //---------------// + /** + * @return the clusterPos + */ + public int getClusterPos () + { + return clusterPos; + } + + //----------// + // getCombs // + //----------// + /** + * @return the combs + */ + public SortedMap getCombs () + { + if (combs != null) { + return combs; + } else { + return new TreeMap<>(); + } + } + + //---------// + // include // + //---------// + /** + * Include a whole other filament into this one + * + * @param that the filament to swallow + */ + public void include (LineFilament that) + { + super.stealSections(that); + + that.cluster = this.cluster; + that.clusterPos = this.clusterPos; + } + + //------------// + // setCluster // + //------------// + /** + * Assign this filament to a line cluster + * + * @param cluster the containing cluster + * @param pos the relative line position within the cluster + */ + public void setCluster (LineCluster cluster, + int pos) + { + this.cluster = cluster; + clusterPos = pos; + } + + //--------------// + // getAlignment // + //--------------// + @Override + protected LineFilamentAlignment getAlignment () + { + return (LineFilamentAlignment) super.getAlignment(); + } + + //-----------------// + // internalsString // + //-----------------// + @Override + protected String internalsString () + { + StringBuilder sb = new StringBuilder(super.internalsString()); + + if (cluster != null) { + sb.append(" cluster:") + .append(cluster.getId()) + .append("p") + .append(clusterPos); + } + + return sb.toString(); + } +} diff --git a/src/main/omr/grid/LineFilamentAlignment.java b/src/main/omr/grid/LineFilamentAlignment.java new file mode 100644 index 0000000..e091aac --- /dev/null +++ b/src/main/omr/grid/LineFilamentAlignment.java @@ -0,0 +1,293 @@ +//----------------------------------------------------------------------------// +// // +// L i n e F i l a m e n t A l i g n m e n t // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.grid; + +import omr.constant.ConstantSet; + +import omr.glyph.facets.Glyph; + +import omr.math.NaturalSpline; + +import omr.run.Orientation; + +import omr.sheet.Scale; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.awt.geom.Point2D; +import java.util.List; + +/** + * Class {@code LineFilamentAlignment} is a GlyphAlignment + * implementation meant for long staff lines filaments. + * + * @author Hervé Bitteur + */ +public class LineFilamentAlignment + extends FilamentAlignment +{ + //~ Static fields/initializers --------------------------------------------- + + /** Specific application parameters */ + private static final Constants constants = new Constants(); + + /** Usual logger utility */ + private static final Logger logger = LoggerFactory.getLogger( + LineFilamentAlignment.class); + + //~ Constructors ----------------------------------------------------------- + // + //-----------------------// + // LineFilamentAlignment // + //-----------------------// + /** + * Creates a new LineFilamentAlignment object. + * + * @param glyph the containing filament + */ + public LineFilamentAlignment (Glyph glyph) + { + super(glyph); + } + + //~ Methods ---------------------------------------------------------------- + // + //-----------// + // fillHoles // + //-----------// + /** + * Fill large holes (due to missing intermediate points) in + * this filament, by interpolating (or extrapolating) from the + * collection of rather parallel fils, this filament is part of + * (at provided pos index). + * + * @param pos the index of this filament in the provided collection + * @param fils the provided collection of parallel filaments + */ + public void fillHoles (int pos, + List fils) + { + Scale scale = new Scale(glyph.getInterline()); + int maxHoleLength = scale.toPixels(constants.maxHoleLength); + int virtualLength = scale.toPixels(constants.virtualSegmentLength); + + // Look for long holes + Double holeStart = null; + boolean modified = false; + + for (int ip = 0; ip < points.size(); ip++) { + Point2D point = points.get(ip); + + if (holeStart == null) { + holeStart = point.getX(); + } else { + double holeStop = point.getX(); + double holeLength = holeStop - holeStart; + + if (holeLength > maxHoleLength) { + // Try to insert artificial intermediate point(s) + int insert = (int) Math.rint(holeLength / virtualLength) + - 1; + + if (insert > 0) { + logger.debug( + "Hole before ip: {} insert:{} for {}", + ip, + insert, + this); + + double dx = holeLength / (insert + 1); + + for (int i = 1; i <= insert; i++) { + int x = (int) Math.rint(holeStart + (i * dx)); + Point2D pt = new Filler( + x, + pos, + fils, + virtualLength / 2).findInsertion(); + + if (pt == null) { + // Take default line point instead + pt = new VirtualPoint( + x, + getPositionAt(x, Orientation.HORIZONTAL)); + } + + logger.debug("Inserted {}", pt); + points.add(ip++, pt); + modified = true; + } + } + } + + holeStart = holeStop; + } + } + + if (modified) { + // Regenerate the underlying curve + line = NaturalSpline.interpolate( + points.toArray(new Point2D[points.size()])); + } + } + + //~ Inner Classes ---------------------------------------------------------- + //-----------// + // Constants // + //-----------// + private static final class Constants + extends ConstantSet + { + //~ Instance fields ---------------------------------------------------- + + final Scale.Fraction virtualSegmentLength = new Scale.Fraction( + 6, + "Typical length used for virtual intermediate points"); + + final Scale.Fraction maxHoleLength = new Scale.Fraction( + 8, + "Maximum length for holes without intermediate points"); + + } + + //--------// + // Filler // + //--------// + /** + * A utility class to fill the filament holes with virtual points + */ + private static class Filler + { + //~ Instance fields ---------------------------------------------------- + + final int x; // Preferred abscissa for point insertion + + final int pos; // Relative position within fils collection + + final List fils; // Collection of fils this one is part of + + final int margin; // Margin on abscissa to lookup refs + + //~ Constructors ------------------------------------------------------- + public Filler (int x, + int pos, + List fils, + int margin) + { + this.x = x; + this.pos = pos; + this.fils = fils; + this.margin = margin; + } + + //~ Methods ------------------------------------------------------------ + //---------------// + // findInsertion // + //---------------// + /** + * Look for a suitable insertion point. + * A point is returned only if it can be computed by interpolation, + * which needs one reference above and one reference below. + * Extrapolation is not reliable enough, so no insertion point is + * returned if we lack reference above or below. + * + * @return the computed insertion point, or null + */ + public Point2D findInsertion () + { + // Check for a reference above + Neighbor one = findNeighbor(fils.subList(0, pos), -1); + + if (one == null) { + return null; + } + + // Check for a reference below + Neighbor two = findNeighbor(fils.subList(pos + 1, fils.size()), 1); + + if (two == null) { + return null; + } + + // Interpolate + double ratio = (double) (pos - one.pos) / (two.pos - one.pos); + + return new VirtualPoint( + ((1 - ratio) * one.point.getX()) + (ratio * two.point.getX()), + ((1 - ratio) * one.point.getY()) + (ratio * two.point.getY())); + } + + /** + * Browse the provided list in the desired direction to find a + * suitable point as a reference in a neighboring filament. + */ + private Neighbor findNeighbor (List subfils, + int dir) + { + final int firstIdx = (dir > 0) ? 0 : (subfils.size() - 1); + final int breakIdx = (dir > 0) ? subfils.size() : (-1); + + for (int i = firstIdx; i != breakIdx; i += dir) { + LineFilament fil = subfils.get(i); + Point2D pt = fil.getAlignment() + .findPoint( + x, + Orientation.HORIZONTAL, + margin); + + if (pt != null) { + return new Neighbor(fil.getClusterPos(), pt); + } + } + + return null; + } + + //~ Inner Classes ------------------------------------------------------ + /** Convey a point together with its relative cluster position */ + private class Neighbor + { + //~ Instance fields ------------------------------------------------ + + final int pos; + + final Point2D point; + + //~ Constructors --------------------------------------------------- + public Neighbor (int pos, + Point2D point) + { + this.pos = pos; + this.point = point; + } + } + } + + //--------------// + // VirtualPoint // + //--------------// + /** + * Used for artificial intermediate points + */ + private static class VirtualPoint + extends Point2D.Double + { + //~ Constructors ------------------------------------------------------- + + public VirtualPoint (double x, + double y) + { + super(x, y); + } + } +} diff --git a/src/main/omr/grid/LineInfo.java b/src/main/omr/grid/LineInfo.java new file mode 100644 index 0000000..adaef0e --- /dev/null +++ b/src/main/omr/grid/LineInfo.java @@ -0,0 +1,109 @@ +//----------------------------------------------------------------------------// +// // +// L i n e I n f o // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.grid; + +import omr.lag.Section; + +import omr.math.Line; + +import omr.util.HorizontalSide; + +import java.awt.Graphics2D; +import java.awt.Rectangle; +import java.awt.geom.Point2D; +import java.util.Collection; + +/** + * Interface {@code LineInfo} describes the handling of one staff line. + * + * @author Hervé Bitteur + */ +public interface LineInfo +{ + //~ Methods ---------------------------------------------------------------- + + /** + * Report the absolute contour rectangle + * + * @return the contour box (with minimum height of 1) + */ + public Rectangle getBounds (); + + /** + * Selector for the left or right ending point of the line + * + * @param side proper horizontal side + * @return left point + */ + public Point2D getEndPoint (HorizontalSide side); + + /** + * Report the id of this line + * + * @return the line id (debugging info) + */ + public int getId (); + + /** + * Selector for the left point of the line + * + * @return left point + */ + public Point2D getLeftPoint (); + + /** + * Selector for the right point of the line + * + * @return right point + */ + public Point2D getRightPoint (); + + /** + * Report the lag sections that compose the staff line + * + * @return a collection of the line sections + */ + public Collection
getSections (); + + /** + * Paint the computed line on the provided environment. + * + * @param g the graphics context + */ + public void render (Graphics2D g); + + /** + * Retrieve the precise intersection with a rather vertical line. + * + * @param vertical the rather vertical line + * @return the precise intersection + */ + public Point2D verticalIntersection (Line vertical); + + /** + * Retrieve the staff line ordinate at given abscissa x, using int + * values + * + * @param x the given abscissa + * @return the corresponding y value + */ + public int yAt (int x); + + /** + * Retrieve the staff line ordinate at given abscissa x, using + * double values + * + * @param x the given abscissa + * @return the corresponding y value + */ + public double yAt (double x); +} diff --git a/src/main/omr/grid/LinesRetriever.java b/src/main/omr/grid/LinesRetriever.java new file mode 100644 index 0000000..95bd884 --- /dev/null +++ b/src/main/omr/grid/LinesRetriever.java @@ -0,0 +1,1028 @@ +//----------------------------------------------------------------------------// +// // +// L i n e s R e t r i e v e r // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.grid; + +import omr.Main; + +import omr.constant.Constant; +import omr.constant.ConstantSet; + +import omr.glyph.Glyphs; +import omr.glyph.facets.Glyph; +import omr.glyph.ui.NestView; + +import omr.lag.BasicLag; +import omr.lag.JunctionRatioPolicy; +import omr.lag.Lag; +import omr.lag.Section; +import omr.lag.SectionsBuilder; + +import omr.run.Orientation; +import static omr.run.Orientation.*; +import omr.run.Run; +import omr.run.RunsTable; +import omr.run.RunsTableFactory; + +import omr.sheet.Scale; +import omr.sheet.Sheet; +import omr.sheet.Skew; +import omr.sheet.SystemInfo; + +import omr.ui.Colors; +import omr.ui.util.UIUtil; +import static omr.util.HorizontalSide.*; +import omr.util.Predicate; +import omr.util.StopWatch; +import omr.util.VipUtil; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.awt.Color; +import java.awt.Dimension; +import java.awt.Graphics2D; +import java.awt.Point; +import java.awt.Rectangle; +import java.awt.Stroke; +import java.awt.geom.Line2D; +import java.awt.geom.Point2D; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * Class {@code LinesRetriever} retrieves the staff lines of a sheet. + * + * @author Hervé Bitteur + */ +public class LinesRetriever + implements NestView.ItemRenderer +{ + //~ Static fields/initializers --------------------------------------------- + + /** Specific application parameters */ + private static final Constants constants = new Constants(); + + /** Usual logger utility */ + private static final Logger logger = LoggerFactory.getLogger(LinesRetriever.class); + + //~ Instance fields -------------------------------------------------------- + // + /** related sheet */ + private final Sheet sheet; + + /** Related scale */ + private final Scale scale; + + /** Scale-dependent constants for horizontal stuff */ + private final Parameters params; + + /** Lag of horizontal runs */ + private Lag hLag; + + /** Filaments factory */ + private FilamentsFactory factory; + + /** Long horizontal filaments found, non sorted */ + private final List filaments = new ArrayList<>(); + + /** Second collection of filaments */ + private List secondFilaments; + + /** Discarded filaments */ + private List discardedFilaments; + + /** Global slope of the sheet */ + private double globalSlope; + + /** Companion in charge of clusters of main interline */ + private ClustersRetriever clustersRetriever; + + /** Companion in charge of clusters of second interline, if any */ + private ClustersRetriever secondClustersRetriever; + + /** Companion in charge of bar lines */ + private final BarsRetriever barsRetriever; + + /** Too-short horizontal runs */ + private RunsTable shortHoriTable; + + /** For runs display, if any */ + private final RunsViewer runsViewer; + + //~ Constructors ----------------------------------------------------------- + // + //----------------// + // LinesRetriever // + //----------------// + /** + * Retrieve the frames of all staff lines. + * + * @param sheet the sheet to process + * @param barsRetriever the companion in charge of bars + */ + public LinesRetriever (Sheet sheet, + BarsRetriever barsRetriever) + { + this.sheet = sheet; + this.barsRetriever = barsRetriever; + + runsViewer = (Main.getGui() != null) + ? new RunsViewer(sheet, this, barsRetriever) : null; + + scale = sheet.getScale(); + params = new Parameters(scale); + } + + //~ Methods ---------------------------------------------------------------- + //----------// + // buildLag // + //----------// + /** + * Build the underlying lag, out of the provided runs table. + * + * @param wholeVertTable the provided table of all (vertical) runs + * @param showRuns (debug) true to create intermediate views on runs + * @return the vertical runs too long to be part of any staff line + */ + public RunsTable buildLag (RunsTable wholeVertTable, + boolean showRuns) + { + hLag = new BasicLag("hLag", Orientation.HORIZONTAL); + + // Create filament factory + try { + factory = new FilamentsFactory( + scale, + sheet.getNest(), + Orientation.HORIZONTAL, + LineFilament.class); + } catch (Exception ex) { + logger.warn("Cannot create lines filament factory", ex); + } + + // To record the purged vertical runs + RunsTable longVertTable = new RunsTable( + "long-vert", + VERTICAL, + new Dimension(sheet.getWidth(), sheet.getHeight())); + + // Remove runs whose height is larger than line thickness + RunsTable shortVertTable = wholeVertTable.copy("short-vert").purge( + new Predicate() + { + @Override + public final boolean check (Run run) + { + return run.getLength() > params.maxVerticalRunLength; + } + }, + longVertTable); + + if (showRuns) { + runsViewer.display(longVertTable); + runsViewer.display(shortVertTable); + } + + // Build table of long horizontal runs + RunsTable wholeHoriTable = new RunsTableFactory( + HORIZONTAL, + shortVertTable.getBuffer(), + 0).createTable("whole-hori"); + + // To record the purged horizontal runs + shortHoriTable = new RunsTable( + "short-hori", + HORIZONTAL, + new Dimension(sheet.getWidth(), sheet.getHeight())); + + RunsTable longHoriTable = wholeHoriTable.copy("long-hori").purge( + new Predicate() + { + @Override + public final boolean check (Run run) + { + return run.getLength() < params.minRunLength; + } + }, + shortHoriTable); + + if (showRuns) { + runsViewer.display(shortHoriTable); + runsViewer.display(longHoriTable); + } + + // Build the horizontal hLag with the long horizontal runs + // (short horizontal runs will be added later) + SectionsBuilder sectionsBuilder = new SectionsBuilder( + hLag, + new JunctionRatioPolicy(params.maxLengthRatio)); + sectionsBuilder.createSections(longHoriTable); + + sheet.setHorizontalLag(hLag); + + setVipSections(); + + return longVertTable; + } + + //---------------// + // completeLines // + //---------------// + /** + * Complete the retrieved staff lines whenever possible with + * filaments and short sections left over. + * + *

Synopsis: + *

+     *      + includeDiscardedFilaments
+     *          + canIncludeFilament(fil1, fil2)
+     *      + createShortSections()
+     *      + includeSections()
+     *          + canIncludeSection(fil, sct)
+     * 
+ */ + public void completeLines () + { + StopWatch watch = new StopWatch("completeLines"); + + try { + // Browse discarded filaments for possible inclusion + watch.start("include discarded filaments"); + includeDiscardedFilaments(); + + // Build sections out of shortHoriTable (too short horizontal runs) + watch.start("create shortSections"); + + List
shortSections = createShortSections(); + + // Dispatch sections into thick & thin ones + watch.start( + "dispatching " + shortSections.size() + " thick / thin"); + + List
thickSections = new ArrayList<>(); + List
thinSections = new ArrayList<>(); + + for (Section section : shortSections) { + if (section.getWeight() > params.maxThinStickerWeight) { + thickSections.add(section); + } else { + thinSections.add(section); + } + } + + // First, consider thick sections and update geometry + watch.start("include " + thickSections.size() + " thick stickers"); + includeSections(thickSections, true); + + // Second, consider thin sections w/o updating the geometry + watch.start("include " + thinSections.size() + " thin stickers"); + includeSections(thinSections, false); + + // Update system coordinates + for (SystemInfo system : sheet.getSystems()) { + system.updateCoordinates(); + } + } finally { + if (constants.printWatch.getValue()) { + watch.print(); + } + } + } + + //-------------// + // renderItems // + //-------------// + /** + * Render the filaments, their ending tangents, their combs + * + * @param g graphics context + */ + @Override + public void renderItems (Graphics2D g) + { + final Stroke oldStroke = UIUtil.setAbsoluteStroke(g, 1f); + final Color oldColor = g.getColor(); + g.setColor(Colors.ENTITY_MINOR); + + // Combs stuff? + if (constants.showCombs.isSet()) { + if (clustersRetriever != null) { + clustersRetriever.renderItems(g); + } + + if (secondClustersRetriever != null) { + secondClustersRetriever.renderItems(g); + } + } + + // Filament lines? + if (constants.showHorizontalLines.isSet()) { + List allFils = new ArrayList<>(filaments); + + if (secondFilaments != null) { + allFils.addAll(secondFilaments); + } + + for (Filament filament : allFils) { + filament.renderLine(g); + } + + // Draw tangent at each ending point? + if (constants.showTangents.isSet()) { + g.setColor(Colors.TANGENT); + + double dx = sheet.getScale().toPixels(constants.tangentLg); + + for (Filament filament : allFils) { + Point2D p = filament.getStartPoint(HORIZONTAL); + double der = filament.slopeAt(p.getX(), HORIZONTAL); + g.draw( + new Line2D.Double( + p.getX(), + p.getY(), + p.getX() - dx, + p.getY() - (der * dx))); + p = filament.getStopPoint(HORIZONTAL); + der = filament.slopeAt(p.getX(), HORIZONTAL); + g.draw( + new Line2D.Double( + p.getX(), + p.getY(), + p.getX() + dx, + p.getY() + (der * dx))); + } + } + } + + g.setStroke(oldStroke); + g.setColor(oldColor); + } + + //---------------// + // retrieveLines // + //---------------// + /** + * Organize the long and thin horizontal sections into filaments + * (glyphs) that will be good candidates for staff lines. + *
    + *
  1. First, retrieve long horizontal sections and merge them into + * filaments.
  2. + *
  3. Second, detect series of filaments regularly spaced and aggregate + * them into clusters of lines (as staff candidates).
  4. + *
+ * + *

Synopsis: + *

+     *      + filamentFactory.retrieveFilaments()
+     *      + retrieveGlobalSlope()
+     *      + clustersRetriever.buildInfo()
+     *      + secondClustersRetriever.buildInfo()
+     *      + buildStaves()
+     * 
+ */ + public void retrieveLines () + { + StopWatch watch = new StopWatch("retrieveLines"); + + try { + // Retrieve filaments out of merged long sections + watch.start("retrieveFilaments"); + + for (Glyph fil : factory.retrieveFilaments( + hLag.getSections(), + true)) { + filaments.add((LineFilament) fil); + } + + // Compute global slope out of longest filaments + watch.start("retrieveGlobalSlope"); + globalSlope = retrieveGlobalSlope(); + sheet.setSkew(new Skew(globalSlope, sheet)); + logger.info("{}Global slope: {}", + sheet.getLogPrefix(), (float) globalSlope); + + // Retrieve regular patterns of filaments and pack them into clusters + clustersRetriever = new ClustersRetriever( + sheet, + filaments, + scale.getInterline(), + Colors.COMB); + watch.start("clustersRetriever"); + + discardedFilaments = clustersRetriever.buildInfo(); + + // Check for a second interline + Integer secondInterline = scale.getSecondInterline(); + + if (secondInterline != null && !discardedFilaments.isEmpty()) { + secondFilaments = discardedFilaments; + Collections.sort(secondFilaments, Glyph.byId); + logger.info("{}Searching clusters with secondInterline: {}", + sheet.getLogPrefix(), secondInterline); + secondClustersRetriever = new ClustersRetriever( + sheet, + secondFilaments, + secondInterline, + Colors.COMB_MINOR); + watch.start("secondClustersRetriever"); + discardedFilaments = secondClustersRetriever.buildInfo(); + } + + logger.debug("Discarded filaments: {}", Glyphs.toString( + discardedFilaments)); + + // Convert clusters into staves + watch.start("BuildStaves"); + buildStaves(); + } finally { + if (constants.printWatch.getValue()) { + watch.print(); + } + } + } + + //-------------// + // buildStaves // + //-------------// + /** + * Register line clusters as staves + */ + private void buildStaves () + { + // Accumulate all clusters, and sort them by ordinate + List allClusters = new ArrayList<>(); + allClusters.addAll(clustersRetriever.getClusters()); + + if (secondClustersRetriever != null) { + allClusters.addAll(secondClustersRetriever.getClusters()); + } + + Collections.sort(allClusters, clustersRetriever.ordinateComparator); + + // Populate the staff manager + StaffManager staffManager = sheet.getStaffManager(); + int staffId = 0; + staffManager.reset(); + + for (LineCluster cluster : allClusters) { + logger.debug(cluster.toString()); + List lines = new ArrayList(cluster.getLines()); + double left = Integer.MAX_VALUE; + double right = Integer.MIN_VALUE; + + for (LineInfo line : lines) { + left = Math.min(left, line.getEndPoint(LEFT).getX()); + right = Math.max(right, line.getEndPoint(RIGHT).getX()); + } + + StaffInfo staff = new StaffInfo( + ++staffId, + left, + right, + new Scale(cluster.getInterline(), scale.getMainFore()), + lines); + staffManager.addStaff(staff); + } + + staffManager.computeStaffLimits(); + + // Polish staff lines + for (StaffInfo staff : staffManager.getStaves()) { + staff.getArea(); + + for (LineInfo l : staff.getLines()) { + FilamentLine line = (FilamentLine) l; + line.fil.polishCurvature(); + } + } + } + + //------------// + // canInclude // + //------------// + /** + * Check whether the staff line filament could include the provided + * entity (section or filament) + * + * @param filament the staff line filament + * @param idStr (debug) entity id + * @param isVip true if entity is vip + * @param box the entity contour box + * @param center the entity center + * @param candidate the section or glyph candidate + * @return true if OK, false otherwise + */ + private boolean canInclude (LineFilament filament, + boolean isVip, + String idStr, + Rectangle box, + Point center, + Object candidate) + { + // For VIP debugging + String vips = null; + + if (isVip) { + vips = idStr + ": "; // BP here! + } + + // Check entity thickness + int height = box.height; + + if (height > params.maxStickerThickness) { + if (logger.isDebugEnabled() || isVip) { + logger.info("{}SSS height:{} vs {}", + vips, height, params.maxStickerThickness); + } + + return false; + } + + // Check entity center gap with theoretical line + double yFil = filament.getPositionAt(center.x, HORIZONTAL); + double dy = Math.abs(yFil - center.y); + double gap = dy - (scale.getMainFore() / 2.0); + + if (gap > params.maxStickerGap) { + if (logger.isDebugEnabled() || isVip) { + logger.info("{}GGG gap:{} vs {}", + vips, (float) gap, (float) params.maxStickerGap); + } + + return false; + } + + // Check max extension from theoretical line + double extension = Math.max( + Math.abs(yFil - box.y), + Math.abs((box.y + height) - yFil)); + + if (extension > params.maxStickerExtension) { + if (logger.isDebugEnabled() || isVip) { + logger.info("{}XXX ext:{} vs {}", + vips, (float) extension, params.maxStickerExtension); + } + + return false; + } + + // Check resulting thickness + double thickness = 0; + + if (candidate instanceof Section) { + thickness = Glyphs.getThicknessAt( + center.x, + HORIZONTAL, + (Section) candidate, + filament); + } else if (candidate instanceof Glyph) { + thickness = Glyphs.getThicknessAt( + center.x, + HORIZONTAL, + (Glyph) candidate, + filament); + } + + if (thickness > params.maxStickerThickness) { + if (logger.isDebugEnabled() || isVip) { + logger.info("{}RRR thickness:{} vs {}", + vips, (float) thickness, params.maxStickerExtension); + } + + return false; + } + + if (logger.isDebugEnabled() || isVip) { + logger.info("{}---", vips); + } + + return true; + } + + //--------------------// + // canIncludeFilament // + //--------------------// + /** + * Check whether the staff line filament could include the candidate + * filament + * + * @param filament the staff line filament + * @param fil the candidate filament + * @return true if OK + */ + private boolean canIncludeFilament (LineFilament filament, + Filament fil) + { + return canInclude( + filament, + fil.isVip(), + "Fil#" + fil.getId(), + fil.getBounds(), + fil.getCentroid(), + fil); + } + + //-------------------// + // canIncludeSection // + //-------------------// + /** + * Check whether the staff line filament could include the candidate + * section + * + * @param filament the staff line filament + * @param section the candidate sticker + * @return true if OK, false otherwise + */ + private boolean canIncludeSection (LineFilament filament, + Section section) + { + return canInclude( + filament, + section.isVip(), + "Sct#" + section.getId(), + section.getBounds(), + section.getCentroid(), + section); + } + + //---------------------// + // createShortSections // + //---------------------// + /** + * Build horizontal sections out of shortHoriTable runs + * + * @return the list of created sections + */ + private List
createShortSections () + { + // Note the current section id + sheet.setLongSectionMaxId(hLag.getLastVertexId()); + + // Augment the horizontal hLag with the short sections + SectionsBuilder sectionsBuilder = new SectionsBuilder( + hLag, + new JunctionRatioPolicy(params.maxLengthRatioShort)); + List
shortSections = sectionsBuilder.createSections( + shortHoriTable); + + setVipSections(); + + return shortSections; + } + + //---------------------------// + // includeDiscardedFilaments // + //---------------------------// + /** + * Last attempt to include discarded filaments to retrieved staff lines + */ + private void includeDiscardedFilaments () + { + // Sort these discarded filaments by top ordinate + Collections.sort(discardedFilaments, Filament.topComparator); + + int iMin = 0; + int iMax = discardedFilaments.size() - 1; + + for (SystemInfo system : sheet.getSystems()) { + for (StaffInfo staff : system.getStaves()) { + for (LineInfo l : staff.getLines()) { + FilamentLine line = (FilamentLine) l; + LineFilament filament = line.fil; + Rectangle lineBox = filament.getBounds(); + lineBox.grow(0, scale.getMainFore()); + + double minX = filament.getStartPoint(HORIZONTAL).getX(); + double maxX = filament.getStopPoint(HORIZONTAL).getX(); + int minY = lineBox.y; + int maxY = lineBox.y + lineBox.height; + + for (int i = iMin; i <= iMax; i++) { + Filament fil = discardedFilaments.get(i); + + if (fil.getPartOf() != null) { + continue; + } + + int firstPos = fil.getBounds().y; + + if (firstPos < minY) { + iMin = i; + + continue; + } + + if (firstPos > maxY) { + break; + } + + Point center = fil.getCentroid(); + + if ((center.x >= minX) && (center.x <= maxX)) { + if (canIncludeFilament(filament, fil)) { + filament.stealSections(fil); + } + } + } + } + } + + barsRetriever.adjustStaffLines(system); + } + } + + //-----------------// + // includeSections // + //-----------------// + /** + * Include "sticker" sections into their related lines, when + * applicable + * + * @param sections List of sections that are stickers candidates + * @param update should we update the line geometry with stickers (this + * should be limited to large sections). + */ + private void includeSections (List
sections, + boolean update) + { + // Sections are sorted according to their top run (Y first and X second) + int iMin = 0; + int iMax = sections.size() - 1; + + // Inclusion on the fly would imply recomputation of filament at each + // section inclusion. So we need to retrieve all "stickers" for a given + // staff line, and perform a global inclusion at the end only. + for (SystemInfo system : sheet.getSystems()) { + for (StaffInfo staff : system.getStaves()) { + for (LineInfo l : staff.getLines()) { + FilamentLine line = (FilamentLine) l; + LineFilament fil = line.fil; + Rectangle lineBox = fil.getBounds(); + lineBox.grow(0, scale.getMainFore()); + + double minX = fil.getStartPoint(HORIZONTAL).getX(); + double maxX = fil.getStopPoint(HORIZONTAL).getX(); + int minY = lineBox.y; + int maxY = lineBox.y + lineBox.height; + List
stickers = new ArrayList<>(); + + for (int i = iMin; i <= iMax; i++) { + Section section = sections.get(i); + + if (section.isGlyphMember()) { + continue; + } + + int firstPos = section.getFirstPos(); + + if (firstPos < minY) { + iMin = i; + + continue; + } + + if (firstPos > maxY) { + break; + } + + Point center = section.getCentroid(); + + if ((center.x >= minX) && (center.x <= maxX)) { + if (canIncludeSection(fil, section)) { + stickers.add(section); + } + } + } + + // Actually include the retrieved stickers? + for (Section section : stickers) { + if (update) { + fil.addSection(section); + } else { + section.setGlyph(fil); + } + } + } + } + + barsRetriever.adjustStaffLines(system); + } + } + + //---------------------// + // retrieveGlobalSlope // + //---------------------// + private double retrieveGlobalSlope () + { + // Use the top longest filaments to determine slope + final double ratio = params.topRatioForSlope; + final int topCount = Math.max( + 1, + (int) Math.rint(filaments.size() * ratio)); + double slopes = 0; + Collections.sort( + filaments, + Glyphs.getReverseLengthComparator(HORIZONTAL)); + + for (int i = 0; i < topCount; i++) { + Filament fil = filaments.get(i); + Point2D start = fil.getStartPoint(HORIZONTAL); + Point2D stop = fil.getStopPoint(HORIZONTAL); + slopes += ((stop.getY() - start.getY()) / (stop.getX() + - start.getX())); + } + + return slopes / topCount; + } + + //----------------// + // setVipSections // + //----------------// + private void setVipSections () + { + // Debug sections VIPs + for (int id : params.vipSections) { + Section sect = hLag.getVertexById(id); + + if (sect != null) { + sect.setVip(); + logger.info("Horizontal vip section: {}", sect); + } + } + } + + //~ Inner Classes ---------------------------------------------------------- + //-----------// + // Constants // + //-----------// + private static final class Constants + extends ConstantSet + { + //~ Instance fields ---------------------------------------------------- + + final Constant.Ratio topRatioForSlope = new Constant.Ratio( + 0.1, + "Percentage of top filaments used to retrieve global slope"); + + // Constants for building horizontal sections + // ------------------------------------------ + final Constant.Ratio maxLengthRatio = new Constant.Ratio( + 1.5, + "Maximum ratio in length for a run to be combined with an existing section"); + + final Constant.Ratio maxLengthRatioShort = new Constant.Ratio( + 3.0, + "Maximum ratio in length for a short run to be combined with an existing section"); + + // Constants specified WRT *maximum* line thickness (scale.getmaxFore()) + // ---------------------------------------------- + // Should be 1.0, unless ledgers are thicker than staff lines + final Constant.Ratio ledgerThickness = new Constant.Ratio( + 1.2, // 2.0, + "Ratio of ledger thickness vs staff line MAXIMUM thickness"); + + final Constant.Ratio stickerThickness = new Constant.Ratio( + 1.0, //1.2, + "Ratio of sticker thickness vs staff line MAXIMUM thickness"); + + // Constants specified WRT mean line thickness + // ------------------------------------------- + // + final Scale.LineFraction maxStickerGap = new Scale.LineFraction( + 0.5, + "Maximum vertical gap between sticker and closest line side"); + + final Scale.LineFraction maxStickerExtension = new Scale.LineFraction( + 1.2, + "Maximum vertical sticker extension from line"); + + final Scale.AreaFraction maxThinStickerWeight = new Scale.AreaFraction( + 0.06, + "Maximum weight for a thin sticker (w/o impact on line geometry)"); + + // Constants specified WRT mean interline + // -------------------------------------- + final Scale.Fraction minRunLength = new Scale.Fraction( + 1.0, + "Minimum length for a horizontal run to be considered"); + + // Constants for display + // --------------------- + final Constant.Boolean showHorizontalLines = new Constant.Boolean( + true, + "Should we display the horizontal lines?"); + + final Scale.Fraction tangentLg = new Scale.Fraction( + 1, + "Typical length to display tangents at ending points"); + + final Constant.Boolean printWatch = new Constant.Boolean( + false, + "Should we print out the stop watch?"); + + final Constant.Boolean showTangents = new Constant.Boolean( + false, + "Should we show filament ending tangents?"); + + // + final Constant.Boolean showCombs = new Constant.Boolean( + false, + "Should we show staff lines combs?"); + + // Constants for debugging + // ----------------------- + final Constant.String horizontalVipSections = new Constant.String( + "", + "(Debug) Comma-separated list of VIP sections"); + } + + //------------// + // Parameters // + //------------// + /** + * Class {@code Parameters} gathers all pre-scaled constants + * related to horizontal frames. + */ + private static class Parameters + { + //~ Instance fields ---------------------------------------------------- + + /** Maximum vertical run length (to exclude too long vertical runs) */ + final int maxVerticalRunLength; + + /** Minimum run length for horizontal lag */ + final int minRunLength; + + /** Used for section junction policy */ + final double maxLengthRatio; + + /** Used for section junction policy for short sections */ + final double maxLengthRatioShort; + + /** Percentage of top filaments used to retrieve global slope */ + final double topRatioForSlope; + + /** Maximum sticker thickness */ + final int maxStickerThickness; + + /** Maximum sticker extension */ + final int maxStickerExtension; + + /** Maximum vertical gap between a sticker and the closest line side */ + final double maxStickerGap; + + /** Maximum weight for a thin sticker */ + final int maxThinStickerWeight; + + // Debug + final List vipSections; + + //~ Constructors ------------------------------------------------------- + /** + * Creates a new Parameters object. + * + * @param scale the scaling factor + */ + public Parameters (Scale scale) + { + // Special parameters + maxVerticalRunLength = (int) Math.rint( + scale.getMaxFore() * constants.ledgerThickness.getValue()); + maxStickerThickness = (int) Math.rint( + scale.getMaxFore() * constants.stickerThickness.getValue()); + + // Others + minRunLength = scale.toPixels(constants.minRunLength); + maxLengthRatio = constants.maxLengthRatio.getValue(); + maxLengthRatioShort = constants.maxLengthRatioShort.getValue(); + topRatioForSlope = constants.topRatioForSlope.getValue(); + maxStickerGap = scale.toPixelsDouble(constants.maxStickerGap); + maxThinStickerWeight = scale.toPixels( + constants.maxThinStickerWeight); + maxStickerExtension = (int) Math.ceil( + scale.toPixelsDouble(constants.maxStickerExtension)); + + // VIPs + vipSections = VipUtil.decodeIds( + constants.horizontalVipSections.getValue()); + + if (logger.isDebugEnabled()) { + Main.dumping.dump(this); + } + + if (!vipSections.isEmpty()) { + logger.info("Horizontal VIP sections: {}", vipSections); + } + } + } +} diff --git a/src/main/omr/grid/RunsViewer.java b/src/main/omr/grid/RunsViewer.java new file mode 100644 index 0000000..306f07e --- /dev/null +++ b/src/main/omr/grid/RunsViewer.java @@ -0,0 +1,117 @@ +//----------------------------------------------------------------------------// +// // +// R u n s V i e w e r // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.grid; + +import omr.run.RunBoard; +import omr.run.RunsTable; +import omr.run.RunsTableView; + +import omr.sheet.Sheet; +import omr.sheet.ui.BinarizationBoard; +import omr.sheet.ui.PixelBoard; + +import omr.ui.BoardsPane; +import omr.ui.view.RubberPanel; +import omr.ui.view.ScrollView; + +import java.awt.Graphics2D; + +/** + * Class {@code RunsViewer} handles the display of instance(s) of + * {@link RunsTable} in the assembly of the related sheet. + * + * @author Hervé Bitteur + */ +public class RunsViewer +{ + //~ Instance fields -------------------------------------------------------- + + /** The related sheet */ + private final Sheet sheet; + + /** The related lines retriever */ + private final LinesRetriever linesRetriever; + + /** The related bars retriever */ + private final BarsRetriever barsRetriever; + + //~ Constructors ----------------------------------------------------------- + //------------// + // RunsViewer // + //------------// + /** + * Creates a new RunsViewer object. + * + * @param sheet the related sheet + * @param linesRetriever the related lines retriever + * @param barsRetriever the related bars retriever + */ + public RunsViewer (Sheet sheet, + LinesRetriever linesRetriever, + BarsRetriever barsRetriever) + { + this.sheet = sheet; + this.linesRetriever = linesRetriever; + this.barsRetriever = barsRetriever; + } + + //~ Methods ---------------------------------------------------------------- + //---------// + // display // + //---------// + /** + * Display a view on provided runs table + * + * @param table the runs to display + */ + public void display (RunsTable table) + { + RubberPanel view = new MyRunsTableView(table); + view.setName(table.getName()); + view.setPreferredSize(table.getDimension()); + + BoardsPane boards = new BoardsPane( + new PixelBoard(sheet), + new BinarizationBoard(sheet), + new RunBoard(table, true)); + + sheet.getAssembly() + .addViewTab(table.getName(), new ScrollView(view), boards); + } + + //~ Inner Classes ---------------------------------------------------------- + //-----------------// + // MyRunsTableView // + //-----------------// + /** + * A specific runs view, which displays retrieved lines and bars + * on top of the runs. + */ + private class MyRunsTableView + extends RunsTableView + { + //~ Constructors ------------------------------------------------------- + + public MyRunsTableView (RunsTable table) + { + super(table, sheet.getLocationService()); + } + + //~ Methods ------------------------------------------------------------ + @Override + protected void renderItems (Graphics2D g) + { + linesRetriever.renderItems(g); + barsRetriever.renderItems(g); + } + } +} diff --git a/src/main/omr/grid/StaffInfo.java b/src/main/omr/grid/StaffInfo.java new file mode 100644 index 0000000..29b332d --- /dev/null +++ b/src/main/omr/grid/StaffInfo.java @@ -0,0 +1,986 @@ +//----------------------------------------------------------------------------// +// // +// S t a f f I n f o // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.grid; + +import omr.glyph.facets.Glyph; +import omr.glyph.ui.AttachmentHolder; +import omr.glyph.ui.BasicAttachmentHolder; + +import omr.math.GeoPath; +import omr.math.LineUtil; +import omr.math.ReversePathIterator; + +import omr.run.Orientation; + +import omr.score.entity.Staff; + +import omr.sheet.NotePosition; +import omr.sheet.Scale; + +import omr.util.HorizontalSide; +import static omr.util.HorizontalSide.*; +import omr.util.VerticalSide; +import static omr.util.VerticalSide.*; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.awt.Graphics2D; +import java.awt.Point; +import java.awt.Shape; +import java.awt.geom.Line2D; +import java.awt.geom.Point2D; +import java.awt.geom.Rectangle2D; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.SortedSet; +import java.util.TreeMap; +import java.util.TreeSet; + +/** + * Class {@code StaffInfo} handles the physical informations of a staff + * with its lines. + * Note: All methods are meant to provide correct results, regardless of the + * actual number of lines in the staff instance. + * + * @author Hervé Bitteur + */ +public class StaffInfo + implements AttachmentHolder +{ + //~ Static fields/initializers --------------------------------------------- + + /** Usual logger utility */ + private static final Logger logger = LoggerFactory.getLogger(StaffInfo.class); + + /** To sort by staff id. */ + public static final Comparator byId = new Comparator() + { + @Override + public int compare (StaffInfo o1, + StaffInfo o2) + { + return Integer.compare(o1.id, o2.id); + } + }; + + //~ Instance fields -------------------------------------------------------- + // + /** Sequence of the staff lines. (from top to bottom) */ + private final List lines; + + /** + * Scale specific to this staff. [not used actually] + * (since different staves in a page may exhibit different scales) + */ + private Scale specificScale; + + /** Top limit of staff related area. (left to right) */ + private GeoPath topLimit = null; + + /** Bottom limit of staff related area. (left to right) */ + private GeoPath bottomLimit = null; + + /** Staff id. counted from 1 within the sheet */ + private final int id; + + /** Information about left bar line. */ + private BarInfo leftBar; + + /** Left extrema. */ + private double left; + + /** Information about right bar line. */ + private BarInfo rightBar; + + /** Right extrema. */ + private double right; + + /** The area around the staff, lazily computed. */ + private GeoPath area; + + /** Map of ledgers nearby. */ + private final Map> ledgerMap = new TreeMap<>(); + + /** Corresponding staff entity in the score hierarchy. */ + private Staff scoreStaff; + + /** Potential attachments. */ + private AttachmentHolder attachments = new BasicAttachmentHolder(); + + //~ Constructors ----------------------------------------------------------- + // + //-----------// + // StaffInfo // + //-----------// + /** + * Create info about a staff, with its contained staff lines. + * + * @param id the id of the staff + * @param left abscissa of the left side + * @param right abscissa of the right side + * @param specificScale specific scale detected for this staff + * @param lines the sequence of contained staff lines + */ + public StaffInfo (int id, + double left, + double right, + Scale specificScale, + List lines) + { + this.id = id; + this.left = (int) Math.rint(left); + this.right = (int) Math.rint(right); + this.specificScale = specificScale; + this.lines = lines; + } + + //~ Methods ---------------------------------------------------------------- + // + //---------------// + // addAttachment // + //---------------// + @Override + public void addAttachment (String id, + Shape attachment) + { + attachments.addAttachment(id, attachment); + } + + //-----------// + // addLedger // + //-----------// + /** + * Add a ledger to the collection (which is lazily created) + * + * @param ledger the ledger to add + * @param index the staff-based index for ledger line + */ + public void addLedger (Glyph ledger, + int index) + { + if (ledger == null) { + throw new IllegalArgumentException("Cannot register a null ledger"); + } + + SortedSet ledgerSet = ledgerMap.get(index); + + if (ledgerSet == null) { + ledgerSet = new TreeSet<>(Glyph.byAbscissa); + ledgerMap.put(index, ledgerSet); + } + + ledgerSet.add(ledger); + } + + //-----------// + // addLedger // + //-----------// + /** + * Add a ledger to the collection, computing line index from + * glyph pitch position. + * + * @param ledger the ledger to add + */ + public void addLedger (Glyph ledger) + { + if (ledger == null) { + throw new IllegalArgumentException("Cannot register a null ledger"); + } + + addLedger(ledger, + getLedgerLineIndex(pitchPositionOf(ledger.getCentroid()))); + } + + //--------------// + // removeLedger // + //--------------// + /** + * Remove a legder from staff collection. + * + * @param ledger the ledger to remove + * @return true if actually removed, false if not found + */ + public boolean removeLedger (Glyph ledger) + { + if (ledger == null) { + throw new IllegalArgumentException("Cannot remove a null ledger"); + } + + // Browse all staff ledger indices + for (SortedSet ledgerSet : ledgerMap.values()) { + if (ledgerSet.remove(ledger)) { + return true; + } + } + + // Not found + logger.debug("Could not find ledger {}", ledger.idString()); + return false; + } + + //------// + // dump // + //------// + /** + * A utility meant for debugging. + */ + public void dump () + { + System.out.println( + "StaffInfo" + getId() + " left=" + left + " right=" + right); + + int i = 0; + + for (LineInfo line : lines) { + System.out.println(" LineInfo" + i++ + " " + line.toString()); + } + } + + //-------------// + // getAbscissa // + //-------------// + /** + * Report the staff abscissa, on the provided side. + * + * @param side provided side + * @return the staff abscissa + */ + public double getAbscissa (HorizontalSide side) + { + if (side == HorizontalSide.LEFT) { + return left; + } else { + return right; + } + } + + //---------// + // getArea // + //---------// + /** + * Report the lazily computed area defined by the staff limits. + * + * @return the whole staff area + */ + public GeoPath getArea () + { + if (area == null) { + area = new GeoPath(); + area.append(topLimit, false); + area.append( + ReversePathIterator.getReversePathIterator(bottomLimit), + true); + area.closePath(); + } + + return area; + } + + //---------------// + // getAreaBounds // + //---------------// + /** + * Report the bounding box of the staff area. + * + * @return the lazily computed bounding box + */ + public Rectangle2D getAreaBounds () + { + return getArea().getBounds2D(); + } + + //----------------// + // getAttachments // + //----------------// + @Override + public Map getAttachments () + { + return attachments.getAttachments(); + } + + //--------// + // getBar // + //--------// + /** + * Report the barline, if any, on the provided side + * + * @param side proper horizontal side + * @return the bar on the provided side, if any + */ + public BarInfo getBar (HorizontalSide side) + { + if (side == HorizontalSide.LEFT) { + return leftBar; + } else { + return rightBar; + } + } + + //------------------// + // getClosestLedger // + //------------------// + /** + * Report the closest ledger (if any) between provided point and + * this staff. + * + * @param point the provided point + * @return the closest ledger found, or null + */ + public IndexedLedger getClosestLedger (Point2D point) + { + IndexedLedger bestLedger = null; + double top = getFirstLine().yAt(point.getX()); + double bottom = getLastLine().yAt(point.getX()); + double rawPitch = (4.0d * ((2 * point.getY()) - bottom - top)) / (bottom + - top); + + if (Math.abs(rawPitch) <= 5) { + return null; + } + + int interline = specificScale.getInterline(); + Rectangle2D searchBox; + + if (rawPitch < 0) { + searchBox = new Rectangle2D.Double( + point.getX(), + point.getY(), + 0, + top - point.getY() + 1); + } else { + searchBox = new Rectangle2D.Double( + point.getX(), + bottom, + 0, + point.getY() - bottom + 1); + } + + //searchBox.grow(interline, interline); + searchBox.setRect( + searchBox.getX() - interline, + searchBox.getY() - interline, + searchBox.getWidth() + (2 * interline), + searchBox.getHeight() + (2 * interline)); + + // Browse all staff ledgers + Set foundLedgers = new HashSet<>(); + for (Map.Entry> entry : ledgerMap.entrySet()) { + for (Glyph ledger : entry.getValue()) { + if (ledger.getBounds().intersects(searchBox)) { + foundLedgers.add(new IndexedLedger(ledger, entry.getKey())); + } + } + } + + if (!foundLedgers.isEmpty()) { + // Use the closest ledger + double bestDist = Double.MAX_VALUE; + + for (IndexedLedger iLedger : foundLedgers) { + Point2D center = iLedger.glyph.getAreaCenter(); + double dist = Math.abs(center.getY() - point.getY()); + + if (dist < bestDist) { + bestDist = dist; + bestLedger = iLedger; + } + } + } + + return bestLedger; + } + + //----------------// + // getClosestLine // + //----------------// + /** + * Report the staff line which is closest to the provided point. + * + * @param point the provided point + * @return the closest line found + */ + public LineInfo getClosestLine (Point2D point) + { + double pos = pitchPositionOf(point); + int idx = (int) Math.rint((pos + (lines.size() - 1)) / 2); + + if (idx < 0) { + idx = 0; + } else if (idx > (lines.size() - 1)) { + idx = lines.size() - 1; + } + + return lines.get(idx); + } + + //----------// + // getGapTo // + //----------// + /** + * Report the vertical gap between staff and the provided glyph. + * + * @param glyph the provided glyph + * @return 0 if the glyph intersects the staff, otherwise the vertical + * distance from staff to closest edge of the glyph + */ + public int getGapTo (Glyph glyph) + { + Point center = glyph.getAreaCenter(); + int staffTop = getFirstLine().yAt(center.x); + int staffBot = getLastLine().yAt(center.x); + int glyphTop = glyph.getBounds().y; + int glyphBot = glyphTop + glyph.getBounds().height - 1; + + // Check overlap + int top = Math.max(glyphTop, staffTop); + int bot = Math.min(glyphBot, staffBot); + if (top <= bot) { + return 0; + } + + // No overlap, compute distance + int dist = Integer.MAX_VALUE; + dist = Math.min(dist, Math.abs(staffTop - glyphTop)); + dist = Math.min(dist, Math.abs(staffTop - glyphBot)); + dist = Math.min(dist, Math.abs(staffBot - glyphTop)); + dist = Math.min(dist, Math.abs(staffBot - glyphBot)); + return dist; + } + + //----------------// + // getEndingSlope // + //----------------// + /** + * Report mean ending slope, on the provided side. + * We discard highest and lowest absolute slopes, and return the average + * values for the remaining ones. + * + * @param side which side to select (left or right) + * @return a "mean" value + */ + public double getEndingSlope (HorizontalSide side) + { + List slopes = new ArrayList<>(lines.size()); + + for (LineInfo l : lines) { + FilamentLine line = (FilamentLine) l; + slopes.add(line.getSlope(side)); + } + + Collections.sort( + slopes, + new Comparator() + { + @Override + public int compare (Double o1, + Double o2) + { + return Double.compare(Math.abs(o1), Math.abs(o2)); + } + }); + + double sum = 0; + + for (Double slope : slopes.subList(1, slopes.size() - 1)) { + sum += slope; + } + + return sum / (slopes.size() - 2); + } + + //--------------// + // getFirstLine // + //--------------// + /** + * Report the first line in the series. + * + * @return the first line + */ + public LineInfo getFirstLine () + { + return lines.get(0); + } + + //-----------// + // getHeight // + //-----------// + /** + * Report the mean height of the staff, between first and last line. + * + * @return the mean staff height + */ + public int getHeight () + { + return getSpecificScale().getInterline() * (lines.size() - 1); + } + + //-------// + // getId // + //-------// + /** + * Report the staff id, counted from 1 in the sheet. + * + * @return the staff id + */ + public int getId () + { + return id; + } + + //-------------// + // getLastLine // + //-------------// + /** + * Report the last line in the series. + * + * @return the last line + */ + public LineInfo getLastLine () + { + return lines.get(lines.size() - 1); + } + + //--------------// + // getLedgerMap // + //--------------// + public Map> getLedgerMap () + { + return ledgerMap; + } + + //------------// + // getLedgers // + //------------// + /** + * Report the ordered set of ledgers, if any, for a given pitch value. + * + * @param lineIndex the precise line index that specifies algebraic + * distance from staff + * @return the proper set of ledgers, or null + */ + public SortedSet getLedgers (int lineIndex) + { + return ledgerMap.get(lineIndex); + } + + //-------------// + // getLimitAtX // + //-------------// + /** + * Report the precise ordinate of staff area limit, on the provided + * vertical side. + * + * @param side the provided vertical side + * @param x the provided abscissa + * @return the ordinate of staff limit + */ + public double getLimitAtX (VerticalSide side, + double x) + { + GeoPath limit = (side == TOP) ? topLimit : bottomLimit; + + return limit.yAtX(x); + } + + //----------// + // getLines // + //----------// + /** + * Report the sequence of lines. + * + * @return the list of lines in this staff + */ + public List getLines () + { + return lines; + } + + //-------------// + // getLinesEnd // + //-------------// + /** + * Report the ending abscissa of the staff lines. + * + * @param side desired horizontal side + * @return the abscissa corresponding to lines extrema + */ + public double getLinesEnd (HorizontalSide side) + { + if (side == HorizontalSide.LEFT) { + double linesLeft = Integer.MAX_VALUE; + + for (LineInfo line : lines) { + linesLeft = Math.min(linesLeft, line.getEndPoint(LEFT).getX()); + } + + return linesLeft; + } else { + double linesRight = Integer.MIN_VALUE; + + for (LineInfo line : lines) { + linesRight = Math.max( + linesRight, + line.getEndPoint(RIGHT).getX()); + } + + return linesRight; + } + } + + //----------------// + // getMidOrdinate // + //----------------// + /** + * Report an approximate ordinate of staff ending, on the provided + * horizontal side. + * + * @param side provided side + * @return the middle ordinate of staff ending + */ + public double getMidOrdinate (HorizontalSide side) + { + return (getFirstLine().getEndPoint(side).getY() + getLastLine(). + getEndPoint(side).getY()) / 2; + } + + //-----------------// + // getNotePosition // + //-----------------// + /** + * Report the precise position for a note-like entity with respect + * to this staff, taking ledgers (if any) into account. + * + * @param point the absolute location of the provided note + * @return the detailed note position + */ + public NotePosition getNotePosition (Point2D point) + { + double pitch = pitchPositionOf(point); + IndexedLedger bestLedger = null; + + // If we are rather far from the staff, try getting help from ledgers + if (Math.abs(pitch) > lines.size()) { + bestLedger = getClosestLedger(point); + + if (bestLedger != null) { + Point2D center = bestLedger.glyph.getAreaCenter(); + int ledgerPitch = getLedgerPitchPosition(bestLedger.index); + double deltaPitch = (2d * (point.getY() - center.getY())) / specificScale. + getInterline(); + pitch = ledgerPitch + deltaPitch; + } + } + + return new NotePosition(this, pitch, bestLedger); + } + + //------------------------// + // getLedgerPitchPosition // + //------------------------// + /** + * Report the pitch position of a ledger WRT the related staff + * + * @param lineIndex the ledger line index + * @return the ledger pitch position + */ + public static int getLedgerPitchPosition (int lineIndex) + { + // // Safer, for the time being... + // if (getStaff() + // .getLines() + // .size() != 5) { + // throw new RuntimeException("Only 5-line staves are supported"); + // } + if (lineIndex > 0) { + return 4 + (2 * lineIndex); + } else { + return -4 + (2 * lineIndex); + } + } + + //--------------------// + // getLedgerLineIndex // + //--------------------// + /** + * Compute staff-based line index, based on provided pitch position + * + * @param pitchPosition the provided pitch position + * @return the computed line index + */ + public static int getLedgerLineIndex (double pitchPosition) + { + if (pitchPosition > 0) { + return (int) Math.rint(pitchPosition / 2) - 2; + } else { + return (int) Math.rint(pitchPosition / 2) + 2; + } + } + + //---------------// + // getScoreStaff // + //---------------// + /** + * Report the related score staff entity. + * + * @return the corresponding scoreStaff + */ + public Staff getScoreStaff () + { + return scoreStaff; + } + + //------------------// + // getSpecificScale // + //------------------// + /** + * Report the specific staff scale, which may have a + * different interline value than the page average. + * + * @return the staff scale + */ + public Scale getSpecificScale () + { + if (specificScale != null) { + // Return the specific scale of this staff + return specificScale; + } else { + // Return the scale of the sheet + logger.warn("No specific scale available"); + + return null; + } + } + + //--------------// + // intersection // + //--------------// + /** + * Report the approximate point where a provided vertical stick + * crosses this staff. + * + * @param stick the rather vertical stick + * @return the crossing point + */ + public Point2D intersection (Glyph stick) + { + LineInfo midLine = lines.get(lines.size() / 2); + + return LineUtil.intersection( + midLine.getEndPoint(LEFT), + midLine.getEndPoint(RIGHT), + stick.getStartPoint(Orientation.VERTICAL), + stick.getStopPoint(Orientation.VERTICAL)); + } + + //-----------------// + // pitchPositionOf // + //-----------------// + /** + * Compute an approximation of the pitch position of a pixel point, + * since it is based only on distance to staff, with no + * consideration for ledgers. + * + * @param pt the pixel point + * @return the pitch position + */ + public double pitchPositionOf (Point2D pt) + { + double top = getFirstLine().yAt(pt.getX()); + double bottom = getLastLine().yAt(pt.getX()); + + return ((lines.size() - 1) * ((2 * pt.getY()) - bottom - top)) / (bottom + - top); + } + + //-------------------// + // removeAttachments // + //-------------------// + @Override + public int removeAttachments (String prefix) + { + return attachments.removeAttachments(prefix); + } + + //--------// + // render // + //--------// + /** + * Paint the staff lines. + * + * @param g the graphics context + * @return true if something has been actually drawn + */ + public boolean render (Graphics2D g) + { + LineInfo firstLine = getFirstLine(); + LineInfo lastLine = getLastLine(); + + if ((firstLine != null) && (lastLine != null)) { + if (g.getClipBounds().intersects(getAreaBounds())) { + // Draw the left and right vertical lines + for (HorizontalSide side : HorizontalSide.values()) { + Point2D first = firstLine.getEndPoint(side); + Point2D last = lastLine.getEndPoint(side); + g.draw(new Line2D.Double(first, last)); + } + + // Draw each horizontal line in the set + for (LineInfo line : lines) { + line.render(g); + } + + return true; + } + } + + return false; + } + + //-------------------// + // renderAttachments // + //-------------------// + @Override + public void renderAttachments (Graphics2D g) + { + attachments.renderAttachments(g); + } + + //-------------// + // setAbscissa // + //-------------// + /** + * Set the staff abscissa of the provided side. + * + * @param side provided side + * @param val abscissa of staff end + */ + public void setAbscissa (HorizontalSide side, + double val) + { + if (side == HorizontalSide.LEFT) { + left = val; + } else { + right = val; + } + } + + //--------// + // setBar // + //--------// + /** + * Set a barline on the provided side + * + * @param side proper horizontal side + * @param bar the bar to set + */ + public void setBar (HorizontalSide side, + BarInfo bar) + { + if (side == HorizontalSide.LEFT) { + this.leftBar = bar; + } else { + this.rightBar = bar; + } + } + + //----------// + // setLimit // + //----------// + /** + * Define the limit of the staff area, on the provided vertical side. + * + * @param side proper vertical side + * @param limit assigned limit + */ + public void setLimit (VerticalSide side, + GeoPath limit) + { + logger.debug("staff#{} setLimit {} {}", id, side, limit); + + if (side == TOP) { + topLimit = limit; + } else { + bottomLimit = limit; + } + + // Invalidate area, so that it gets recomputed when needed + area = null; + } + + //---------------// + // setScoreStaff // + //---------------// + /** + * Remember the related score staff entity. + * + * @param scoreStaff the corresponding scoreStaff to set + */ + public void setScoreStaff (Staff scoreStaff) + { + this.scoreStaff = scoreStaff; + } + + //----------// + // toString // + //----------// + @Override + public String toString () + { + StringBuilder sb = new StringBuilder("{StaffInfo"); + + sb.append(" id=").append(getId()); + sb.append(" left=").append((float) left); + sb.append(" right=").append((float) right); + + if (specificScale != null) { + sb.append(" specificScale=").append(specificScale.getInterline()); + } + + if (leftBar != null) { + sb.append(" leftBar:").append(leftBar); + } + + if (rightBar != null) { + sb.append(" rightBar:").append(rightBar); + } + + sb.append("}"); + + return sb.toString(); + } + + //---------------// + // IndexedLedger // + //---------------// + public static class IndexedLedger + { + + /** The ledger glyph. */ + public final Glyph glyph; + + /** Staff-based line index. (-1, -2, ... above, +1, +2, ... below) */ + public final int index; + + public IndexedLedger (Glyph ledger, + int index) + { + this.glyph = ledger; + this.index = index; + } + } +} diff --git a/src/main/omr/grid/StaffManager.java b/src/main/omr/grid/StaffManager.java new file mode 100644 index 0000000..04a9986 --- /dev/null +++ b/src/main/omr/grid/StaffManager.java @@ -0,0 +1,352 @@ +//----------------------------------------------------------------------------// +// // +// S t a f f M a n a g e r // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.grid; + +import omr.constant.ConstantSet; + +import omr.math.GeoPath; + +import omr.sheet.Scale; +import omr.sheet.Sheet; + +import omr.util.Navigable; +import static omr.util.VerticalSide.*; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.awt.Graphics2D; +import java.awt.geom.Line2D; +import java.awt.geom.Point2D; +import java.awt.geom.Rectangle2D; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * Class {@code StaffManager} handles physical information about all + * the staves of a given sheet. + * + * @author Hervé Bitteur + */ +public class StaffManager +{ + //~ Static fields/initializers --------------------------------------------- + + /** Specific application parameters */ + private static final Constants constants = new Constants(); + + /** Usual logger utility */ + private static final Logger logger = LoggerFactory.getLogger(StaffManager.class); + + //~ Instance fields -------------------------------------------------------- + // + /** The related sheet */ + @Navigable(false) + private final Sheet sheet; + + /** The sequence of staves, from top to bottom */ + private final List staves = new ArrayList<>(); + + /** The systems tops per staff */ + private Integer[] systemTops; + + /** The parts tops per staff */ + private Integer[] partTops; + + //~ Constructors ----------------------------------------------------------- + // + //--------------// + // StaffManager // + //--------------// + /** + * Creates a new StaffManager object. + * + * @param sheet the related sheet + */ + public StaffManager (Sheet sheet) + { + this.sheet = sheet; + } + + //~ Methods ---------------------------------------------------------------- + // + //----------// + // addStaff // + //----------// + /** + * Append one staff to the current collection + * + * @param staff the staff to add + */ + public void addStaff (StaffInfo staff) + { + staves.add(staff); + } + + //--------------------// + // computeStaffLimits // + //--------------------// + public void computeStaffLimits () + { + final int width = sheet.getWidth(); + final int height = sheet.getHeight(); + StaffInfo prevStaff = null; + double samplingDx = sheet.getScale() + .toPixelsDouble(constants.samplingDx); + final int sampleCount = (int) Math.rint(width / samplingDx); + samplingDx = width / sampleCount; + + for (StaffInfo staff : staves) { + if (prevStaff == null) { + // Very first staff + staff.setLimit( + TOP, + new GeoPath(new Line2D.Double(0, 0, width, 0))); + } else { + // Define a middle line between last line of previous staff + // and first line of current staff + LineInfo prevLine = prevStaff.getLastLine(); + LineInfo nextLine = staff.getFirstLine(); + GeoPath middle = new GeoPath(); + + for (int i = 0; i <= sampleCount; i++) { + int x = (int) Math.rint(i * samplingDx); + double y = (prevLine.yAt(x) + nextLine.yAt(x)) / 2; + + if (i == 0) { + middle.moveTo(x, y); + } else { + middle.lineTo(x, y); + } + } + + // Point on right side + middle.lineTo(width, (prevLine.yAt(width) + nextLine.yAt(width)) / 2); + + prevStaff.setLimit(BOTTOM, middle); + staff.setLimit(TOP, middle); + } + + // Remember this staff for next one + prevStaff = staff; + } + + // Bottom of last staff + prevStaff.setLimit( + BOTTOM, + new GeoPath(new Line2D.Double(0, height, width, height))); + } + + //------------// + // getIndexOf // + //------------// + ///TODO @Deprecated + public int getIndexOf (StaffInfo staff) + { + return staves.indexOf(staff); + } + + //----------// + // getRange // + //----------// + /** + * Report a view on the range of staves from first to last + * (both inclusive). + * + * @param first the first staff of the range + * @param last the last staff of the range + * @return a view on this range + */ + public List getRange (StaffInfo first, + StaffInfo last) + { + return staves.subList(getIndexOf(first), getIndexOf(last) + 1); + } + + //----------// + // getStaff // + //----------// + ///TODO @Deprecated + public StaffInfo getStaff (int index) + { + return staves.get(index); + } + + //------------// + // getStaffAt // + //------------// + /** + * Report the staff, among the sequence provided, whose area + * contains the provided point. + * + * @param point the provided point + * @param theStaves the staves sequence to search + * @return the containing staff, or null if none found + */ + public static StaffInfo getStaffAt (Point2D point, + List theStaves) + { + for (StaffInfo staff : theStaves) { + Rectangle2D box = staff.getAreaBounds(); + + if (point.getY() > box.getMaxY()) { + continue; + } + + if (point.getY() < box.getMinY()) { + // Point above first staff, use first staff + // TODO: this decision is questionable + return null; //staff; + } + + // If the point is ON the area boundary, it is NOT contained. + // So we use a rectangle of 1x1 pixels + if (staff.getArea() + .intersects(point.getX(), point.getY(), 1, 1)) { + return staff; + } + } + + // Point below last staff, use last staff + // TODO: this decision is questionable + return null; //theStaves.get(theStaves.size() - 1); + } + + //------------// + // getStaffAt // + //------------// + /** + * Report the staff whose area contains the provided point + * + * @param point the provided point + * @return the nearest staff, or null if none found + */ + public StaffInfo getStaffAt (Point2D point) + { + return getStaffAt(point, staves); + } + + //---------------// + // getStaffCount // + //---------------// + /** + * Report the total number of staves, whatever their containing + * systems. + * + * @return the count of staves + */ + public int getStaffCount () + { + return staves.size(); + } + + //-----------// + // getStaves // + //-----------// + /** + * Report an unmodifiable view (perhaps empty) of list of current + * staves. + * + * @return a view on staves + */ + public List getStaves () + { + return Collections.unmodifiableList(staves); + } + + //--------// + // render // + //--------// + /** + * Paint all the staff lines + * + * @param g the graphics context (including current color and stroke) + */ + public void render (Graphics2D g) + { + for (StaffInfo staff : staves) { + staff.renderAttachments(g); + } + } + //-------------// + // getPartTops // + //-------------// + + /** + * @return the partTops + */ + public Integer[] getPartTops () + { + return partTops; + } + + //-------------// + // setPartTops // + //-------------// + /** + * @param partTops the partTops to set + */ + public void setPartTops (Integer[] partTops) + { + this.partTops = partTops; + } + + //---------------// + // getSystemTops // + //---------------// + /** + * @return the systemTops + */ + public Integer[] getSystemTops () + { + return systemTops; + } + + //---------------// + // setSystemTops // + //---------------// + /** + * @param systemTops the systemTops to set + */ + public void setSystemTops (Integer[] systemTops) + { + this.systemTops = systemTops; + } + + //-------// + // reset // + //-------// + /** + * Empty the whole collection of staves. + */ + public void reset () + { + staves.clear(); + } + + //~ Inner Classes ---------------------------------------------------------- + // + //-----------// + // Constants // + //-----------// + private static final class Constants + extends ConstantSet + { + //~ Instance fields ---------------------------------------------------- + + Scale.Fraction samplingDx = new Scale.Fraction( + 4d, + "Abscissa sampling to compute top & bottom limits of staff areas"); + + } +} diff --git a/src/main/omr/grid/StickIntersection.java b/src/main/omr/grid/StickIntersection.java new file mode 100644 index 0000000..945ea53 --- /dev/null +++ b/src/main/omr/grid/StickIntersection.java @@ -0,0 +1,149 @@ +//----------------------------------------------------------------------------// +// // +// S t i c k I n t e r s e c t i o n // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.grid; + +import omr.glyph.facets.Glyph; + +import java.awt.geom.Point2D; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Comparator; +import java.util.List; + +/** + * Class {@code StickIntersection} records the intersection point of a + * stick with a crossing line. + * (A typical example is a vertical barline stick that crosses a horizontal + * staff line) + * + * @author Hervé Bitteur + */ +public class StickIntersection + implements Comparable +{ + //~ Static fields/initializers --------------------------------------------- + + /** Comparator on increasing abscissa */ + public static Comparator byAbscissa = new Comparator() + { + @Override + public int compare (StickIntersection o1, + StickIntersection o2) + { + int dx = Double.compare(o1.x, o2.x); + + if (dx != 0) { + return dx; + } else { + // Just to disambiguate + return Double.compare(o1.y, o2.y); + } + } + }; + + /** Comparator on increasing ordinate */ + public static Comparator byOrdinate = new Comparator() + { + @Override + public int compare (StickIntersection o1, + StickIntersection o2) + { + int dy = Double.compare(o1.y, o2.y); + + if (dy != 0) { + return dy; + } else { + // Just to disambiguate + return Double.compare(o1.x, o2.x); + } + } + }; + + //~ Instance fields -------------------------------------------------------- + /** Abscissa where the stick intersects the line */ + public final double x; + + /** Ordinate where the stick intersects the line */ + public final double y; + + /** The stick */ + private final Glyph stick; + + //~ Constructors ----------------------------------------------------------- + /** + * Creates a new StickIntersection object. + * + * @param loc absolute location of the intersection + * @param stick the related stick + */ + public StickIntersection (Point2D loc, + Glyph stick) + { + this.x = loc.getX(); + this.y = loc.getY(); + this.stick = stick; + } + + //~ Methods ---------------------------------------------------------------- + //-----------// + // compareTo // + //-----------// + /** For sorting sticks on abscissa, for a given staff */ + @Override + public int compareTo (StickIntersection that) + { + int dx = Double.compare(x, that.x); + + if (dx != 0) { + return dx; + } else { + // Just to disambiguate + return Double.compare(y, that.y); + } + } + + //----------// + // sticksOf // + //----------// + /** Conversion to a sequence of sticks */ + public static List sticksOf (Collection sps) + { + List sticks = new ArrayList<>(); + + for (StickIntersection sp : sps) { + sticks.add(sp.getStickAncestor()); + } + + return sticks; + } + + //------------------// + // getStickAncestor // + //------------------// + /** + * @return the stick, in fact its ancestor + */ + public Glyph getStickAncestor () + { + return stick.getAncestor(); + } + + //----------// + // toString // + //----------// + @Override + public String toString () + { + return getStickAncestor() + .idString() + "@x:" + (float) x + ",y:" + (float) y; + } +} diff --git a/src/main/omr/grid/TargetBuilder.java b/src/main/omr/grid/TargetBuilder.java new file mode 100644 index 0000000..574dbfb --- /dev/null +++ b/src/main/omr/grid/TargetBuilder.java @@ -0,0 +1,486 @@ +//----------------------------------------------------------------------------// +// // +// T a r g e t B u i l d e r // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.grid; + +import omr.Main; + +import omr.constant.Constant; +import omr.constant.ConstantSet; + +import omr.score.ScoresManager; + +import omr.sheet.Scale; +import omr.sheet.Sheet; +import omr.sheet.Skew; +import omr.sheet.SystemInfo; +import omr.sheet.picture.jai.JaiDewarper; + +import omr.ui.Colors; +import omr.ui.view.RubberPanel; +import omr.ui.view.ScrollView; + +import omr.util.HorizontalSide; +import static omr.util.HorizontalSide.*; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.awt.BasicStroke; +import java.awt.Dimension; +import java.awt.Graphics; +import java.awt.Graphics2D; +import java.awt.Stroke; +import java.awt.geom.AffineTransform; +import java.awt.geom.Path2D; +import java.awt.geom.Point2D; +import java.awt.geom.Rectangle2D; +import java.awt.image.RenderedImage; +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import javax.imageio.ImageIO; + +/** + * Class {@code TargetBuilder} is in charge of building a "perfect" + * definition of target systems, staves and lines as well as the + * dewarp grid that allows to transform the original image in to the + * perfect image. + * + * @author Hervé Bitteur + */ +public class TargetBuilder +{ + //~ Static fields/initializers --------------------------------------------- + + /** Specific application parameters */ + private static final Constants constants = new Constants(); + + /** Usual logger utility */ + private static final Logger logger = LoggerFactory.getLogger(TargetBuilder.class); + + //~ Instance fields -------------------------------------------------------- + /** Related sheet */ + private final Sheet sheet; + + /** Target width */ + private double targetWidth; + + /** Target height */ + private double targetHeight; + + /** Transform from initial point to deskewed point */ + private AffineTransform at; + + /** The target page */ + private TargetPage targetPage; + + /** All target lines */ + private List allTargetLines = new ArrayList<>(); + + /** Source points */ + private List srcPoints = new ArrayList<>(); + + /** Destination points */ + private List dstPoints = new ArrayList<>(); + + //~ Constructors ----------------------------------------------------------- + //---------------// + // TargetBuilder // + //---------------// + /** + * Creates a new TargetBuilder object. + * + * @param sheet the related sheet + */ + public TargetBuilder (Sheet sheet) + { + this.sheet = sheet; + } + + //~ Methods ---------------------------------------------------------------- + //-----------// + // buildInfo // + //-----------// + public void buildInfo () + { + buildTarget(); + + JaiDewarper dewarper = new JaiDewarper(sheet); + + buildWarpGrid(dewarper); + + // Dewarp the initial image + RenderedImage dewarpedImage = dewarper.dewarpImage(); + + // Add a view on dewarped image? + if (Main.getGui() != null) { + sheet.getAssembly() + .addViewTab( + "Dewarped", + new ScrollView(new DewarpedView(dewarpedImage)), + null); + } + + // Store dewarped image on disk + if (constants.storeDewarp.getValue()) { + storeImage(dewarpedImage); + } + } + + //---------------// + // renderSystems // + //---------------// + /** + * TODO: This should be done from a more central class + * + * @param g graphical context + */ + public void renderSystems (Graphics2D g) + { + Scale scale = sheet.getScale(); + Skew skew = sheet.getSkew(); + // Make sure we are not painting changing data... + if (scale == null || skew == null) { + return; + } + + double absDx = scale.toPixelsDouble(constants.systemMarkWidth); + double absDy = skew.getSlope() * absDx; + Stroke systemStroke = new BasicStroke( + (float) scale.toPixelsDouble(constants.systemMarkStroke), + BasicStroke.CAP_ROUND, + BasicStroke.JOIN_ROUND); + + g.setStroke(systemStroke); + g.setColor(Colors.SYSTEM_BRACKET); + + for (SystemInfo system : sheet.getSystems()) { + for (HorizontalSide side : HorizontalSide.values()) { + Point2D top = system.getFirstStaff() + .getFirstLine() + .getEndPoint(side); + Point2D bot = system.getLastStaff() + .getLastLine() + .getEndPoint(side); + + // Draw something like a vertical bracket + double dx = (side == LEFT) ? (-absDx) : absDx; + double dy = (side == LEFT) ? (-absDy) : absDy; + Path2D p = new Path2D.Double(); + p.moveTo(top.getX(), top.getY()); + p.lineTo(top.getX() + dx, top.getY() + dy); + p.lineTo(bot.getX() + dx, bot.getY() + dy); + p.lineTo(bot.getX(), bot.getY()); + g.draw(p); + } + } + } + + //----------------// + // renderWarpGrid // + //----------------// + /** + * Render the grid used to dewarp the sheet image + * + * @param g the graphic context + * @param useSource true to renderAttachments the source grid, false to + * renderAttachments the + * destination grid + */ + public void renderWarpGrid (Graphics g, + boolean useSource) + { + if (!constants.displayGrid.getValue()) { + return; + } + + Graphics2D g2 = (Graphics2D) g; + List points = useSource ? srcPoints : dstPoints; + double radius = sheet.getScale() + .toPixelsDouble(constants.gridPointSize); + g2.setColor(Colors.WARP_POINT); + + Rectangle2D rect = new Rectangle2D.Double(); + + for (Point2D pt : points) { + rect.setRect( + pt.getX() - radius, + pt.getY() - radius, + 2 * radius, + 2 * radius); + g2.fill(rect); + } + } + + //-------------// + // buildTarget // + //-------------// + /** + * Build a perfect definition of target page, systems, staves and + * lines. + * + * We apply a rotation on every top-left corner + */ + private void buildTarget () + { + final Skew skew = sheet.getSkew(); + + // Target page parameters + targetPage = new TargetPage(targetWidth, targetHeight); + + TargetLine prevLine = null; // Latest staff line + + // Target system parameters + for (SystemInfo system : sheet.getSystems()) { + StaffInfo firstStaff = system.getFirstStaff(); + LineInfo firstLine = firstStaff.getFirstLine(); + Point2D dskLeft = skew.deskewed(firstLine.getEndPoint(LEFT)); + Point2D dskRight = skew.deskewed(firstLine.getEndPoint(RIGHT)); + + if (prevLine != null) { + // Preserve position relative to bottom left of previous system + Point2D prevDskLeft = skew.deskewed( + prevLine.info.getEndPoint(LEFT)); + TargetSystem prevSystem = prevLine.staff.system; + double dx = prevSystem.left - prevDskLeft.getX(); + double dy = prevLine.y - prevDskLeft.getY(); + dskLeft.setLocation(dskLeft.getX() + dx, dskLeft.getY() + dy); + dskRight.setLocation( + dskRight.getX() + dx, + dskRight.getY() + dy); + } + + TargetSystem targetSystem = new TargetSystem( + system, + dskLeft.getY(), + dskLeft.getX(), + dskRight.getX()); + targetPage.systems.add(targetSystem); + + // Target staff parameters + for (StaffInfo staff : system.getStaves()) { + dskLeft = skew.deskewed(staff.getFirstLine().getEndPoint(LEFT)); + + if (prevLine != null) { + // Preserve inter-staff vertical gap + Point2D prevDskLeft = skew.deskewed( + prevLine.info.getEndPoint(LEFT)); + dskLeft.setLocation( + dskLeft.getX(), + dskLeft.getY() + (prevLine.y - prevDskLeft.getY())); + } + + TargetStaff targetStaff = new TargetStaff( + staff, + dskLeft.getY(), + targetSystem); + targetSystem.staves.add(targetStaff); + + // Target line parameters + int lineIdx = -1; + + for (LineInfo line : staff.getLines()) { + lineIdx++; + + // Enforce perfect staff interline + TargetLine targetLine = new TargetLine( + line, + targetStaff.top + + (staff.getSpecificScale().getInterline() * lineIdx), + targetStaff); + allTargetLines.add(targetLine); + targetStaff.lines.add(targetLine); + prevLine = targetLine; + } + } + } + } + + //---------------// + // buildWarpGrid // + //---------------// + private void buildWarpGrid (JaiDewarper dewarper) + { + int xStep = sheet.getInterline(); + int xNumCells = (int) Math.ceil(sheet.getWidth() / (double) xStep); + int yStep = sheet.getInterline(); + int yNumCells = (int) Math.ceil(sheet.getHeight() / (double) yStep); + + for (int ir = 0; ir <= yNumCells; ir++) { + for (int ic = 0; ic <= xNumCells; ic++) { + Point2D dst = new Point2D.Double(ic * xStep, ir * yStep); + dstPoints.add(dst); + + Point2D src = sourceOf(dst); + srcPoints.add(src); + } + } + + float[] warpPositions = new float[srcPoints.size() * 2]; + int i = 0; + + for (Point2D p : srcPoints) { + warpPositions[i++] = (float) p.getX(); + warpPositions[i++] = (float) p.getY(); + } + + dewarper.createWarpGrid( + 0, + xStep, + xNumCells, + 0, + yStep, + yNumCells, + warpPositions); + } + + //----------// + // sourceOf // + //----------// + /** + * This key method provides the source point (in original sheet image) + * that corresponds to a given destination point (in target dewarped image). + * + * The strategy is to stay consistent with the staff lines nearby which + * are used as grid references. + * + * @param dst the given destination point + * @return the corresponding source point + */ + private Point2D sourceOf (Point2D dst) + { + double dstX = dst.getX(); + double dstY = dst.getY(); + + // Retrieve north & south lines, if any + TargetLine northLine = null; + TargetLine southLine = null; + + for (TargetLine line : allTargetLines) { + if (line.y <= dstY) { + northLine = line; + } else { + southLine = line; + + break; + } + } + + // Case of image top: no northLine + if (northLine == null) { + return southLine.sourceOf(dst); + } + + // Case of image bottom: no southLine + if (southLine == null) { + return northLine.sourceOf(dst); + } + + // Normal case: use y barycenter between projections sources + Point2D srcNorth = northLine.sourceOf(dstX); + Point2D srcSouth = southLine.sourceOf(dstX); + double yRatio = (dstY - northLine.y) / (southLine.y - northLine.y); + + return new Point2D.Double( + ((1 - yRatio) * srcNorth.getX()) + (yRatio * srcSouth.getX()), + ((1 - yRatio) * srcNorth.getY()) + (yRatio * srcSouth.getY())); + } + + //------------// + // storeImage // + //------------// + private void storeImage (RenderedImage dewarpedImage) + { + String pageId = sheet.getPage().getId(); + File file = new File( + ScoresManager.getInstance().getDefaultDewarpDirectory(), + pageId + ".dewarped.png"); + + try { + String path = file.getCanonicalPath(); + ImageIO.write(dewarpedImage, "png", file); + logger.info("Wrote {}", path); + } catch (IOException ex) { + logger.warn("Could not write {}", file); + } + } + + //~ Inner Classes ---------------------------------------------------------- + //-----------// + // Constants // + //-----------// + private static final class Constants + extends ConstantSet + { + //~ Instance fields ---------------------------------------------------- + + Constant.Boolean displayGrid = new Constant.Boolean( + false, + "Should we display the dewarp grid?"); + + Scale.LineFraction gridPointSize = new Scale.LineFraction( + 0.2, + "Size of displayed grid points"); + + Scale.Fraction systemMarkWidth = new Scale.Fraction( + 2.0, + "Width of system marks"); + + Scale.LineFraction systemMarkStroke = new Scale.LineFraction( + 2.0, + "Thickness of system marks"); + + Constant.Boolean storeDewarp = new Constant.Boolean( + false, + "Should we store the dewarped image on disk?"); + + } + + //--------------// + // DewarpedView // + //--------------// + private class DewarpedView + extends RubberPanel + { + //~ Instance fields ---------------------------------------------------- + + private final AffineTransform identity = new AffineTransform(); + + private final RenderedImage image; + + //~ Constructors ------------------------------------------------------- + public DewarpedView (RenderedImage image) + { + this.image = image; + + setModelSize(new Dimension(image.getWidth(), image.getHeight())); + + // Location service + setLocationService(sheet.getLocationService()); + + setName("DewarpedView"); + } + + //~ Methods ------------------------------------------------------------ + @Override + public void render (Graphics2D g) + { + // Display the dewarped image + g.drawRenderedImage(image, identity); + + // Display also the Destination Points + renderWarpGrid(g, false); + } + } +} diff --git a/src/main/omr/grid/TargetLine.java b/src/main/omr/grid/TargetLine.java new file mode 100644 index 0000000..42d4fd1 --- /dev/null +++ b/src/main/omr/grid/TargetLine.java @@ -0,0 +1,138 @@ +//----------------------------------------------------------------------------// +// // +// T a r g e t L i n e // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.grid; + +import static omr.util.HorizontalSide.*; + +import java.awt.geom.Point2D; + +/** + * Class {@code TargetLine} is an immutable perfect destination object + * for a staff line. + * + * @author Hervé Bitteur + */ +public class TargetLine +{ + //~ Instance fields -------------------------------------------------------- + + /** Related raw information */ + public final LineInfo info; + + /** Id for debug */ + public final int id; + + /** Ordinate in containing page */ + public final double y; + + /** Containing staff */ + public final TargetStaff staff; + + /** Sine of raw line angle */ + private final double sin; + + /** Cosine of raw line angle */ + private final double cos; + + //~ Constructors ----------------------------------------------------------- + //------------// + // TargetLine // + //------------// + /** + * Creates a new TargetLine object. + * + * @param info the physical information + * @param y ordinate in containing pag + * @param staff the containing staff + */ + public TargetLine (LineInfo info, + double y, + TargetStaff staff) + { + this.info = info; + this.y = y; + this.staff = staff; + + id = info.getId(); + + // Compute sin & cos values + Point2D left = info.getEndPoint(LEFT); + Point2D right = info.getEndPoint(RIGHT); + double dx = right.getX() - left.getX(); + double dy = right.getY() - left.getY(); + double hypot = Math.hypot(dx, dy); + sin = dy / hypot; + cos = dx / hypot; + } + + //~ Methods ---------------------------------------------------------------- + //----------// + // sourceOf // + //----------// + /** + * Report the source point that corresponds to a destination point dst + * above or below this line + * + * @param dst the given destination point + * @return the corresponding source point + */ + public Point2D sourceOf (Point2D dst) + { + // Use orthogonal projection to line + double dist = dst.getY() - y; + Point2D projSrc = sourceOf(dst.getX()); + double dx = -dist * sin; + double dy = dist * cos; + + return new Point2D.Double(projSrc.getX() + dx, projSrc.getY() + dy); + } + + //----------// + // sourceOf // + //----------// + /** + * Report the source point that corresponds to a destination point at + * abscissa dstX on this line + * + * @param dstX the given destination abscissa + * @return the corresponding source point + */ + public Point2D sourceOf (double dstX) + { + double left = staff.system.left; + double right = staff.system.right; + double xRatio = (dstX - left) / (right - left); + double srcX = ((1 - xRatio) * info.getEndPoint(LEFT) + .getX()) + + (xRatio * info.getEndPoint(RIGHT) + .getX()); + double srcY = info.yAt(srcX); + + return new Point2D.Double(srcX, srcY); + } + + //----------// + // toString // + //----------// + @Override + public String toString () + { + StringBuilder sb = new StringBuilder("{Line"); + sb.append("#") + .append(id); + sb.append(" y:") + .append(y); + sb.append("}"); + + return sb.toString(); + } +} diff --git a/src/main/omr/grid/TargetPage.java b/src/main/omr/grid/TargetPage.java new file mode 100644 index 0000000..c20248f --- /dev/null +++ b/src/main/omr/grid/TargetPage.java @@ -0,0 +1,71 @@ +//----------------------------------------------------------------------------// +// // +// T a r g e t P a g e // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.grid; + +import java.util.ArrayList; +import java.util.List; + +/** + * Class {@code TargetPage} is an immutable perfect destination object + * for a page. + * + * @author Hervé Bitteur + */ +public class TargetPage +{ + //~ Instance fields -------------------------------------------------------- + + /** Page width */ + public final double width; + + /** Page height */ + public final double height; + + /** Sequence of systems */ + public final List systems = new ArrayList<>(); + + //~ Constructors ----------------------------------------------------------- + //------------// + // TargetPage // + //------------// + /** + * Creates a new TargetPage object. + * + * @param width page width + * @param height page height + */ + public TargetPage (double width, + double height) + { + this.width = width; + this.height = height; + } + + //~ Methods ---------------------------------------------------------------- + //----------// + // toString // + //----------// + @Override + public String toString () + { + StringBuilder sb = new StringBuilder("{Page"); + + sb.append(" width:") + .append(width); + sb.append(" height:") + .append(height); + + sb.append("}"); + + return sb.toString(); + } +} diff --git a/src/main/omr/grid/TargetStaff.java b/src/main/omr/grid/TargetStaff.java new file mode 100644 index 0000000..8952e37 --- /dev/null +++ b/src/main/omr/grid/TargetStaff.java @@ -0,0 +1,79 @@ +//----------------------------------------------------------------------------// +// // +// T a r g e t S t a f f // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.grid; + +import java.util.ArrayList; +import java.util.List; + +/** + * Class {@code TargetStaff} is an immutable perfect destination + * object for a staff. + * + * @author Hervé Bitteur + */ +public class TargetStaff +{ + //~ Instance fields -------------------------------------------------------- + + /** Initial raw information */ + public final StaffInfo info; + + /** Id for debug */ + public final int id; + + /** Ordinate of top in containing page */ + public final double top; + + /** Sequence of staff lines */ + public final List lines = new ArrayList<>(); + + /** Containing system */ + public final TargetSystem system; + + //~ Constructors ----------------------------------------------------------- + //-------------// + // TargetStaff // + //-------------// + /** + * Creates a new TargetStaff object. + * + * @param info initial raw information + * @param top Ordinate of top in containing page + */ + public TargetStaff (StaffInfo info, + double top, + TargetSystem system) + { + this.info = info; + this.top = top; + this.system = system; + + id = info.getId(); + } + + //~ Methods ---------------------------------------------------------------- + //----------// + // toString // + //----------// + @Override + public String toString () + { + StringBuilder sb = new StringBuilder("{Staff"); + sb.append("#") + .append(id); + sb.append(" top:") + .append(top); + sb.append("}"); + + return sb.toString(); + } +} diff --git a/src/main/omr/grid/TargetSystem.java b/src/main/omr/grid/TargetSystem.java new file mode 100644 index 0000000..54b0459 --- /dev/null +++ b/src/main/omr/grid/TargetSystem.java @@ -0,0 +1,92 @@ +//----------------------------------------------------------------------------// +// // +// T a r g e t S y s t e m // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.grid; + +import omr.sheet.SystemInfo; + +import java.util.ArrayList; +import java.util.List; + +/** + * Class {@code TargetSystem} is an immutable perfect destination + * object for a system. + * + * @author Hervé Bitteur + */ +public class TargetSystem +{ + //~ Instance fields -------------------------------------------------------- + + /** Raw information */ + public final SystemInfo info; + + /** Id for debug */ + public final int id; + + /** Ordinate of top of first staff in containing page */ + public final double top; + + /** Left abscissa in containing page */ + public final double left; + + /** Right abscissa in containing page */ + public final double right; + + /** Sequence of staves */ + public final List staves = new ArrayList<>(); + + //~ Constructors ----------------------------------------------------------- + //--------------// + // TargetSystem // + //--------------// + /** + * Creates a new TargetSystem object. + * + * @param info the original raw information + * @param top ordinate of top + * @param left abscissa of left + * @param right abscissa of right + */ + public TargetSystem (SystemInfo info, + double top, + double left, + double right) + { + this.info = info; + this.top = top; + this.left = left; + this.right = right; + + id = info.getId(); + } + + //~ Methods ---------------------------------------------------------------- + //----------// + // toString // + //----------// + @Override + public String toString () + { + StringBuilder sb = new StringBuilder("{System"); + sb.append("#") + .append(id); + sb.append(" top:") + .append(top); + sb.append(" left:") + .append(left); + sb.append(" right:") + .append(right); + sb.append("}"); + + return sb.toString(); + } +} diff --git a/src/main/omr/grid/doc-files/cluster.uxf b/src/main/omr/grid/doc-files/cluster.uxf new file mode 100644 index 0000000..fb77280 --- /dev/null +++ b/src/main/omr/grid/doc-files/cluster.uxf @@ -0,0 +1,237 @@ + + + 10 + + com.umlet.element.Class + + 20 + 310 + 90 + 50 + + *LineCluster* +bg=#ffff55 +-- +contourBox + + + + com.umlet.element.Relation + + 80 + 280 + 120 + 70 + + lt=<<<<-> +lines + 30;50;100;50 + + + com.umlet.element.Class + + 180 + 310 + 120 + 50 + + *FilamentLine* +bg=#ffff55 +-- + + + + com.umlet.element.Class + + 370 + 220 + 100 + 50 + + *Filament* +bg=#ffff55 +-- +refDist + + + + com.umlet.element.Relation + + 270 + 280 + 120 + 70 + + lt=-> +fil + 30;50;100;50 + + + com.umlet.element.Class + + 580 + 310 + 130 + 70 + + *FilamentPattern* +bg=#ffff55 +-- +ys +processed + + + + com.umlet.element.Relation + + 440 + 280 + 160 + 70 + + lt=<<<<-> +patterns + 30;50;140;50 + + + com.umlet.element.Relation + + 60 + 240 + 108 + 100 + + lt=-> +parent + 50;80;60;80;60;50;30;50;30;70 + + + com.umlet.element.Relation + + 420 + 0 + 118 + 110 + + lt=-> +partOf + 50;90;70;90;70;50;30;50;30;70 + + + com.umlet.element.Class + + 370 + 70 + 100 + 40 + + <<interface>> +/Stick/ + + + + com.umlet.element.Relation + + 390 + 80 + 50 + 90 + + lt=<<. + 30;30;30;70 + + + com.umlet.element.Relation + + 440 + 300 + 160 + 70 + + lt=<->>>> +filaments + 30;50;140;50 + + + com.umlet.element.Class + + 370 + 150 + 100 + 30 + + BasicStick + + + + com.umlet.element.Relation + + 390 + 150 + 50 + 90 + + lt=<<- + 30;30;30;70 + + + com.umlet.element.Class + + 370 + 310 + 100 + 50 + + *LineFilament* +bg=#ffff55 +-- +clusterPos + + + + com.umlet.element.Relation + + 390 + 240 + 50 + 90 + + lt=<<- + 30;30;30;70 + + + com.umlet.element.Relation + + 30 + 330 + 414 + 90 + + lt=<- +r2=cluster + 30;30;30;70;390;70;390;30 + + + com.umlet.element.Relation + + 210 + 240 + 50 + 90 + + lt=<<. + 30;30;30;70 + + + com.umlet.element.Class + + 190 + 230 + 100 + 40 + + <<interface>> +/LineInfo/ + + + diff --git a/src/main/omr/grid/doc-files/filament.uxf b/src/main/omr/grid/doc-files/filament.uxf new file mode 100644 index 0000000..25e1798 --- /dev/null +++ b/src/main/omr/grid/doc-files/filament.uxf @@ -0,0 +1,367 @@ + + + 10 + + com.umlet.element.Class + + 100 + 260 + 100 + 90 + + *Filament* +bg=#ffff55 +-- +refDist + + + + com.umlet.element.Class + + 390 + 360 + 60 + 60 + + Point2D +-- +x +y + + + + com.umlet.element.Relation + + 390 + 270 + 50 + 110 + + lt=<<<<-> +r1=points + 30;30;30;90 + + + com.umlet.element.Class + + 160 + 30 + 80 + 40 + + <<interface>> +/Stick/ + + + + com.umlet.element.Relation + + 150 + 40 + 50 + 140 + + lt=<<. + 30;30;30;120 + + + com.umlet.element.Class + + 530 + 270 + 100 + 30 + + NaturalSpline +bg=#ffff55 + + + + com.umlet.element.Relation + + 420 + 250 + 130 + 50 + + lt=-> +r1=(line) + + 30;30;110;30 + + + com.umlet.element.Class + + 100 + 160 + 100 + 30 + + BasicStick + + + + com.umlet.element.Relation + + 120 + 160 + 50 + 120 + + lt=<<- + 30;30;30;100 + + + com.umlet.element.Class + + 310 + 30 + 110 + 40 + + <<interface>> +/GlyphAlignment/ + + + + com.umlet.element.Class + + 300 + 160 + 120 + 60 + + BasicAlignment +-- +pStart +pStop + + + + + com.umlet.element.Relation + + 330 + 40 + 50 + 140 + + lt=<<. + 30;30;30;120 + + + com.umlet.element.Relation + + 170 + 140 + 150 + 50 + + lt=-> +r1=alignment + 30;30;130;30 + + + com.umlet.element.Class + + 530 + 160 + 80 + 30 + + BasicLine + + + + com.umlet.element.Class + + 560 + 30 + 80 + 40 + + <<interface>> +/Line/ + + + + com.umlet.element.Relation + + 570 + 40 + 50 + 140 + + lt=<<. + 30;30;30;120 + + + com.umlet.element.Relation + + 390 + 140 + 160 + 50 + + lt=-> +r1=line + 30;30;140;30 + + + com.umlet.element.Relation + + 590 + 40 + 50 + 250 + + lt=<<. + 30;30;30;230 + + + com.umlet.element.Class + + 300 + 270 + 150 + 30 + + *FilamentAlignment* +bg=#ffff55 + + + + com.umlet.element.Relation + + 330 + 190 + 50 + 100 + + lt=<<- + 30;30;30;80 + + + com.umlet.element.Class + + 40 + 70 + 100 + 60 + + BasicGlyph +-- +getInterline() +getPartOf() + + + + com.umlet.element.Relation + + 90 + 100 + 50 + 80 + + lt=<<- + 30;30;30;60 + + + com.umlet.element.Relation + + 170 + 250 + 150 + 50 + + lt=-> +r1=(alignment) + 30;30;130;30 + + + com.umlet.element.Class + + 100 + 450 + 100 + 70 + + *LineFilament* +bg=#ffff55 +-- +clusterPos +-- +fillHoles() + + + + com.umlet.element.Relation + + 120 + 320 + 50 + 150 + + lt=<<- + 30;30;30;130 + + + com.umlet.element.Class + + 300 + 460 + 180 + 30 + + *LineFilamentAlignment* +bg=#ffff55 + + + + com.umlet.element.Relation + + 170 + 440 + 150 + 50 + + lt=-> +r1=(alignment) + 30;30;130;30 + + + com.umlet.element.Relation + + 330 + 270 + 50 + 210 + + lt=<<- + 30;30;30;190 + + + com.umlet.element.Class + + 140 + 580 + 100 + 30 + + *LineCluster* + + + + + com.umlet.element.Relation + + 150 + 490 + 64 + 110 + + lt=-> +r1=cluster + 40;30;40;90 + + diff --git a/src/main/omr/grid/doc-files/grid.uxf b/src/main/omr/grid/doc-files/grid.uxf new file mode 100644 index 0000000..f98552d --- /dev/null +++ b/src/main/omr/grid/doc-files/grid.uxf @@ -0,0 +1,189 @@ + + + 10 + + com.umlet.element.Class + + 220 + 320 + 120 + 100 + + *TargetBuilder* +bg=#ffff55 +-- +targetPage +dewarpGrid +dewarpedImage +-- +buildInfo() + + + + com.umlet.element.Class + + 30 + 170 + 120 + 50 + + *GridBuilder* +bg=#ffff55 +-- +-- +buildInfo() + + + + com.umlet.element.Class + + 220 + 20 + 120 + 120 + + *LinesRetriever* +bg=#ffff55 +-- +hLag +filaments +globalSlope +-- +buildLag() +buildInfo() + + + + com.umlet.element.Class + + 220 + 170 + 120 + 120 + + *BarsRetriever* +bg=#ffff55 +-- +vLag +bars +systems +-- +buildLag() +buildInfo() + + + + com.umlet.element.Relation + + 120 + 0 + 120 + 200 + + lt=-> + 30;180;100;30 + + + com.umlet.element.Relation + + 120 + 150 + 120 + 50 + + lt=-> + 30;30;100;30 + + + com.umlet.element.Relation + + 120 + 150 + 120 + 200 + + lt=-> + 30;30;100;180 + + + com.umlet.element.Class + + 400 + 20 + 140 + 140 + + *ClustersRetriever* +bg=#ffff55 +-- +interline +sheetTopEdge +colPatterns +popLength +clusters +-- +buildInfo() + + + + com.umlet.element.Relation + + 310 + 0 + 110 + 50 + + lt=<<<<-> + 30;30;90;30 + + + com.umlet.element.Class + + 400 + 170 + 140 + 70 + + *BarsChecker* +bg=#ffff55 +-- +suite +-- +retrieveCandidates() + + + + com.umlet.element.Relation + + 310 + 150 + 110 + 50 + + lt=-> + 30;30;90;30 + + + com.umlet.element.Class + + 30 + 260 + 120 + 30 + + *GridView* +bg=#ff5555 + + + + com.umlet.element.Relation + + 60 + 190 + 50 + 90 + + lt=-> + 30;30;30;70 + + diff --git a/src/main/omr/grid/doc-files/pixel.uxf b/src/main/omr/grid/doc-files/pixel.uxf new file mode 100644 index 0000000..c628d8f --- /dev/null +++ b/src/main/omr/grid/doc-files/pixel.uxf @@ -0,0 +1,387 @@ + + + 10 + + com.umlet.element.Relation + + 380 + 30 + 50 + 100 + + lt=<- + 30;80;30;30 + + + com.umlet.element.Relation + + 380 + 120 + 50 + 90 + + lt=<- + 30;70;30;30 + + + com.umlet.element.custom.Systemborder + + 370 + 190 + 80 + 40 + + *whole-vert* +bg=blue +RunsTable + + + + com.umlet.element.Relation + + 380 + 200 + 50 + 90 + + lt=<- + 30;70;30;30 + + + com.umlet.element.custom.Systemborder + + 200 + 340 + 80 + 40 + + *short-vert* +bg=blue +RunsTable + + + + com.umlet.element.custom.Systemborder + + 520 + 340 + 90 + 40 + + *purged-vert* +bg=blue +RunsTable + + + + com.umlet.element.custom.Decision + + 390 + 270 + 40 + 40 + + Height > 1.75 * Line +bg=green + + + + com.umlet.element.Relation + + 400 + 260 + 190 + 100 + + lt=<- +m2=Height > 1.75 * Line + 170;80;170;30;30;30 + + + com.umlet.element.Relation + + 210 + 260 + 200 + 100 + + lt=<- +m2=Height <= 1.75 * Line + 30;80;30;30;180;30 + + + com.umlet.element.custom.Systemborder + + 200 + 500 + 80 + 40 + + *whole-hori* +bg=red +RunsTable + + + + com.umlet.element.custom.Decision + + 220 + 580 + 40 + 40 + + Height > 1.75 * Line +bg=green + + + + com.umlet.element.Relation + + 210 + 510 + 50 + 90 + + lt=<- + 30;70;30;30 + + + com.umlet.element.Relation + + 70 + 570 + 170 + 100 + + lt=<- +m2=Length >= min + 30;80;30;30;150;30 + + + com.umlet.element.custom.Systemborder + + 60 + 650 + 80 + 40 + + *long-hori* +bg=red +RunsTable + + + + com.umlet.element.custom.Systemborder + + 220 + 800 + 50 + 30 + + *hLag* +bg=pink + + + + com.umlet.element.custom.Systemborder + + 550 + 510 + 50 + 30 + + *vLag* +bg=pink + + + + com.umlet.element.custom.Systemborder + + 330 + 650 + 90 + 40 + + *purged-hori* +bg=red +RunsTable + + + + com.umlet.element.Relation + + 230 + 570 + 160 + 100 + + lt=<- +m2=Length < min + 140;80;140;30;30;30 + + + com.umlet.element.Relation + + 210 + 350 + 50 + 90 + + lt=<- + 30;70;30;30 + + + com.umlet.element.Relation + + 540 + 350 + 50 + 90 + + lt=<- + 30;70;30;30 + + + com.umlet.element.Relation + + 540 + 430 + 50 + 100 + + lt=<- + 30;80;30;30 + + + com.umlet.element.Relation + + 210 + 430 + 50 + 90 + + lt=<- + 30;70;30;30 + + + com.umlet.element.Relation + + 70 + 660 + 50 + 90 + + lt=<- + 30;70;30;30 + + + com.umlet.element.Relation + + 70 + 740 + 170 + 90 + + lt=<- +m1=first + 150;70;30;70;30;30 + + + com.umlet.element.UseCase + + 350 + 110 + 120 + 40 + + lt=. +Thresholding +bg=white + + + + com.umlet.element.UseCase + + 180 + 420 + 120 + 40 + + lt=. +vert -> hori + + + + com.umlet.element.UseCase + + 510 + 420 + 120 + 40 + + lt=. +Table -> Lag + + + + com.umlet.element.UseCase + + 40 + 730 + 120 + 40 + + lt=. +Table -> Lag + + + + com.umlet.element.custom.State + + 350 + 20 + 120 + 40 + + *Original* +bg=gray +Image + + + + + com.umlet.element.UseCase + + 300 + 730 + 150 + 40 + + lt=. +Add short sections + + + + com.umlet.element.Relation + + 340 + 660 + 50 + 90 + + lt=<- + 30;70;30;30 + + + com.umlet.element.Relation + + 240 + 740 + 150 + 90 + + lt=<- +m1=second + 30;70;130;70;130;30 + + diff --git a/src/main/omr/grid/doc-files/target.uxf b/src/main/omr/grid/doc-files/target.uxf new file mode 100644 index 0000000..59a096a --- /dev/null +++ b/src/main/omr/grid/doc-files/target.uxf @@ -0,0 +1,122 @@ + + + 10 + + com.umlet.element.Class + + 20 + 30 + 90 + 60 + + *TargetPage* +-- +width +height + + + + com.umlet.element.Relation + + 80 + 10 + 90 + 50 + + lt=<<<<-> + 30;30;70;30 + + + com.umlet.element.Class + + 150 + 30 + 120 + 80 + + *TargetSystem* +-- +top +left +right + + + + com.umlet.element.Class + + 310 + 30 + 100 + 50 + + *TargetStaff* +-- +top + + + + com.umlet.element.Relation + + 240 + 10 + 90 + 50 + + lt=<<<<- + 30;30;70;30 + + + com.umlet.element.Class + + 450 + 30 + 100 + 50 + + *TargetLine* +-- +y + + + + com.umlet.element.Relation + + 380 + 10 + 90 + 50 + + lt=<<<<- + 30;30;70;30 + + + com.umlet.element.Class + + 310 + 120 + 100 + 150 + + *StaffInfo* +-- +specificScale +topLimit +bottomLimit +left +leftBar +right +rightBar + + + + com.umlet.element.Relation + + 330 + 50 + 50 + 90 + + lt=-> + 30;30;30;70 + + diff --git a/src/main/omr/grid/package.html b/src/main/omr/grid/package.html new file mode 100644 index 0000000..d6ecb5a --- /dev/null +++ b/src/main/omr/grid/package.html @@ -0,0 +1,31 @@ + + + + + + Package omr.sheet.ui + + + +

+ Package dedicated to the retrieval of sheet grid of systems. +

+ +

Main components:
+ +

+ +

+ +

+ +

+ +

+ +

+ +

+ + diff --git a/src/main/omr/lag/BasicLag.java b/src/main/omr/lag/BasicLag.java new file mode 100644 index 0000000..56cd051 --- /dev/null +++ b/src/main/omr/lag/BasicLag.java @@ -0,0 +1,616 @@ +//----------------------------------------------------------------------------// +// // +// B a s i c L a g // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.lag; + +import omr.glyph.facets.Glyph; +import omr.glyph.ui.ViewParameters; + +import omr.graph.BasicDigraph; + +import omr.run.Orientation; +import omr.run.Run; +import omr.run.RunsTable; + +import omr.selection.GlyphEvent; +import omr.selection.LagEvent; +import omr.selection.LocationEvent; +import omr.selection.MouseMovement; +import omr.selection.RunEvent; +import omr.selection.SectionEvent; +import omr.selection.SectionIdEvent; +import omr.selection.SectionSetEvent; +import omr.selection.SelectionHint; +import omr.selection.SelectionService; +import omr.selection.UserEvent; + +import omr.util.Predicate; + +import org.bushe.swing.event.EventSubscriber; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.awt.Rectangle; +import java.util.ArrayList; +import java.util.Collection; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; + +/** + * Class {@code BasicLag} is a basic implementation of {@link Lag} + * interface. + * + * @author Hervé Bitteur + */ +public class BasicLag + extends BasicDigraph + implements Lag, + EventSubscriber +{ + //~ Static fields/initializers --------------------------------------------- + + /** Usual logger utility */ + private static final Logger logger = LoggerFactory.getLogger(BasicLag.class); + + /** Events read on location service */ + public static final Class[] locEventsRead = new Class[]{LocationEvent.class}; + + /** Events read on run service */ + public static final Class[] runEventsRead = new Class[]{RunEvent.class}; + + /** Events read on section service */ + public static final Class[] sctEventsRead = new Class[]{ + SectionIdEvent.class, + SectionEvent.class + }; + + //~ Instance fields -------------------------------------------------------- + /** Orientation of the lag */ + private final Orientation orientation; + + /** Underlying runs table */ + private RunsTable runsTable; + + /** Location service */ + private SelectionService locationService; + + /** Hosted section service */ + protected final SelectionService lagService; + + /** Scene service */ + private SelectionService glyphService; + + //~ Constructors ----------------------------------------------------------- + //----------// + // BasicLag // + //----------// + /** + * Constructor with specified orientation + * + * @param name the distinguished name for this instance + * @param orientation the desired orientation of the lag + */ + public BasicLag (String name, + Orientation orientation) + { + this(name, BasicSection.class, orientation); + } + + //----------// + // BasicLag // + //----------// + /** + * Constructor with specified orientation and section class + * + * @param name the distinguished name for this instance + * @param orientation the desired orientation of the lag + */ + public BasicLag (String name, + Class sectionClass, + Orientation orientation) + { + super(name, sectionClass); + this.orientation = orientation; + lagService = new SelectionService(name, Lag.eventsWritten); + } + + //~ Methods ---------------------------------------------------------------- + //---------// + // addRuns // + //---------// + @Override + public void addRuns (RunsTable runsTable) + { + if (this.runsTable == null) { + this.runsTable = runsTable.copy(); + } else { + // Add runs into the existing table + this.runsTable.include(runsTable); + } + } + + //---------------// + // createSection // + //---------------// + @Override + public Section createSection (int firstPos, + Run firstRun) + { + if (firstRun == null) { + throw new IllegalArgumentException("null first run"); + } + + Section section = createVertex(); + section.setFirstPos(firstPos); + section.append(firstRun); + + return section; + } + + //----------------// + // getOrientation // + //----------------// + @Override + public Orientation getOrientation () + { + return orientation; + } + + //----------// + // getRunAt // + //----------// + @Override + public final Run getRunAt (int x, + int y) + { + return runsTable.getRunAt(x, y); + } + + //---------------// + // getRunService // + //---------------// + @Override + public SelectionService getRunService () + { + return runsTable.getRunService(); + } + + //---------// + // getRuns // + //---------// + @Override + public RunsTable getRuns () + { + return runsTable; + } + + //-------------------// + // getSectionService // + //-------------------// + @Override + public SelectionService getSectionService () + { + return lagService; + } + + //-------------// + // getSections // + //-------------// + @Override + public final Collection
getSections () + { + return getVertices(); + } + + //--------------------// + // getSelectedSection // + //--------------------// + @Override + public Section getSelectedSection () + { + return (Section) getSectionService().getSelection(SectionEvent.class); + } + + //-----------------------// + // getSelectedSectionSet // + //-----------------------// + @Override + @SuppressWarnings("unchecked") + public Set
getSelectedSectionSet () + { + return (Set
) getSectionService().getSelection( + SectionSetEvent.class); + } + + //------------// + // isVertical // + //------------// + /** + * Predicate on lag orientation + * + * @return true if vertical, false if horizontal + */ + public boolean isVertical () + { + return orientation.isVertical(); + } + + //---------------------------// + // lookupIntersectedSections // + //---------------------------// + @Override + public Set
lookupIntersectedSections (Rectangle rect) + { + return Sections.lookupIntersectedSections(rect, getSections()); + } + + //----------------// + // lookupSections // + //----------------// + @Override + public Set
lookupSections (Rectangle rect) + { + return Sections.lookupSections(rect, getSections()); + } + + //---------// + // onEvent // + //---------// + @Override + public void onEvent (UserEvent event) + { + try { + // Ignore RELEASING + if (event.movement == MouseMovement.RELEASING) { + return; + } + + if (event instanceof LocationEvent) { + // Location => lassoed Section(s) + handleEvent((LocationEvent) event); + } else if (event instanceof RunEvent) { + // Run => Section + handleEvent((RunEvent) event); + } else if (event instanceof SectionIdEvent) { + // Section ID => Section + handleEvent((SectionIdEvent) event); + } else if (event instanceof SectionEvent) { + // Section => contour & SectionSet update + Glyph? + handleEvent((SectionEvent) event); + } + } catch (Exception ex) { + logger.warn(getClass().getName() + " onEvent error", ex); + } + } + + //---------// + // publish // + //---------// + /** + * Publish on Lag selection service + * + * @param event the event to publish + */ + public void publish (LagEvent event) + { + lagService.publish(event); + } + + //---------// + // publish // + //---------// + /** + * Publish a RunEvent on RunsTable service + * + * @param event the event to publish + */ + public void publish (RunEvent event) + { + // Delegate to RunsTable + getRunService().publish(event); + } + + //---------// + // publish // + //---------// + public void publish (LocationEvent locationEvent) + { + locationService.publish(locationEvent); + } + + //---------------// + // purgeSections // + //---------------// + @Override + public List
purgeSections (Predicate
predicate) + { + // List of sections to be purged (to avoid concurrent modifications) + List
purges = new ArrayList<>(2000); + + // Iterate on all sections + for (Section section : getSections()) { + // Check predicate on the current section + if (predicate.check(section)) { + logger.debug("Purging {}", section); + purges.add(section); + } + } + + // Now, actually perform the needed removals + for (Section section : purges) { + section.delete(); + + // Remove the related runs from the underlying runsTable + int pos = section.getFirstPos(); + + for (Run run : section.getRuns()) { + runsTable.removeRun(pos++, run); + } + } + + // Return the sections purged + return purges; + } + + //---------// + // setRuns // + //---------// + @Override + public void setRuns (RunsTable runsTable) + { + if (this.runsTable != null) { + throw new RuntimeException("Attempt to overwrite lag runs table"); + } else { + this.runsTable = runsTable; + } + } + + //-------------// + // setServices // + //-------------// + @Override + public void setServices (SelectionService locationService, + SelectionService sceneService) + { + this.locationService = locationService; + this.glyphService = sceneService; + + runsTable.setLocationService(locationService); + + for (Class eventClass : locEventsRead) { + locationService.subscribeStrongly(eventClass, this); + } + + for (Class eventClass : runEventsRead) { + getRunService().subscribeStrongly(eventClass, this); + } + + for (Class eventClass : sctEventsRead) { + lagService.subscribeStrongly(eventClass, this); + } + } + + //-------------// + // cutServices // + //-------------// + @Override + public void cutServices () + { + runsTable.cutLocationService(locationService); + + for (Class eventClass : locEventsRead) { + locationService.unsubscribe(eventClass, this); + } + + for (Class eventClass : runEventsRead) { + getRunService().unsubscribe(eventClass, this); + } + + for (Class eventClass : sctEventsRead) { + lagService.unsubscribe(eventClass, this); + } + } + + //-----------------// + // internalsString // + //-----------------// + @Override + protected String internalsString () + { + StringBuilder sb = new StringBuilder(super.internalsString()); + + // Orientation + sb.append(" ").append(orientation); + + // // Runs + // if (runsTable != null) { + // sb.append((" runs:")) + // .append(runsTable.getRunCount()); + // } + return sb.toString(); + } + + //-------------// + // handleEvent // + //-------------// + /** + * Interest in lasso SheetLocation => Section(s) + * + * @param sheetLocation + */ + private void handleEvent (LocationEvent locationEvent) + { + logger.debug("Lag. sheetLocation:{}", locationEvent); + + Rectangle rect = locationEvent.getData(); + + if (rect == null) { + return; + } + + SelectionHint hint = locationEvent.hint; + MouseMovement movement = locationEvent.movement; + + if (!hint.isLocation() && !hint.isContext()) { + return; + } + + // Section selection mode? + if (ViewParameters.getInstance().isSectionMode()) { + // Non-degenerated rectangle? + if ((rect.width > 0) && (rect.height > 0)) { + // Look for enclosed sections + Set
sectionsFound = lookupSections(rect); + + // Publish (first) Section found + Section section = sectionsFound.isEmpty() ? null + : sectionsFound.iterator().next(); + publish(new SectionEvent(this, hint, movement, section)); + + // Publish whole SectionSet + publish( + new SectionSetEvent(this, hint, movement, sectionsFound)); + } + } + } + + //-------------// + // handleEvent // + //-------------// + /** + * Interest in Run => Section + * + * @param run + */ + private void handleEvent (RunEvent runEvent) + { + logger.debug("Lag. run:{}", runEvent); + + // Lookup for Section linked to this Run + // Search and forward section info + Run run = runEvent.getData(); + + SelectionHint hint = runEvent.hint; + MouseMovement movement = runEvent.movement; + + if (!hint.isLocation() && !hint.isContext()) { + return; + } + + // Publish Section information + Section section = (run != null) ? run.getSection() : null; + publish(new SectionEvent(this, hint, movement, section)); + } + + //-------------// + // handleEvent // + //-------------// + /** + * Interest in SectionId => Section + * + * @param idEvent + */ + private void handleEvent (SectionIdEvent idEvent) + { + Integer id = idEvent.getData(); + + if ((id == null) || (id == 0)) { + return; + } + + SelectionHint hint = idEvent.hint; + MouseMovement movement = idEvent.movement; + + // Always publish a null Run + publish(new RunEvent(this, hint, movement, null)); + + // Lookup a lag section with proper ID + publish(new SectionEvent(this, hint, movement, getVertexById(id))); + } + + //-------------// + // handleEvent // + //-------------// + /** + * Interest in Section => section contour + update SectionSet + * + * @param sectionEvent + */ + private void handleEvent (SectionEvent sectionEvent) + { + SelectionHint hint = sectionEvent.hint; + MouseMovement movement = sectionEvent.movement; + Section section = sectionEvent.getData(); + + if (hint == SelectionHint.SECTION_INIT) { + // Publish section contour + publish( + new LocationEvent( + this, + hint, + null, + (section != null) ? section.getBounds() : null)); + } + + // In section-selection mode, update section set + if (ViewParameters.getInstance().isSectionMode()) { + // Section mode: Update section set + Set
sections = getSelectedSectionSet(); + + if (sections == null) { + sections = new LinkedHashSet<>(); + } + + if (hint == SelectionHint.LOCATION_ADD) { + if (section != null) { + if (movement == MouseMovement.PRESSING) { + // Adding to (or Removing from) the set of sections + if (sections.contains(section)) { + sections.remove(section); + } else { + sections.add(section); + } + } else if (movement == MouseMovement.DRAGGING) { + // Always adding to the set of sections + sections.add(section); + } + } + } else { + // Overwriting the set of sections + if (section != null) { + // Make a one-section set + sections.clear(); + sections.add(section); + } else if (!sections.isEmpty()) { + // Empty the section set + sections.clear(); + } + } + + logger.debug("{}. Publish section set {}", getName(), sections); + publish(new SectionSetEvent(this, hint, movement, sections)); + } else if (glyphService != null) { + // Section -> Glyph + if (hint.isLocation() || hint.isContext() || hint.isSection()) { + // Select related Glyph if any + Glyph glyph = (section != null) ? section.getGlyph() : null; + + if (glyph != null) { + logger.debug("{}. Publish glyph {}", getName(), glyph); + glyphService.publish( + new GlyphEvent(this, hint, movement, glyph)); + } + } + } + } +} diff --git a/src/main/omr/lag/BasicRoi.java b/src/main/omr/lag/BasicRoi.java new file mode 100644 index 0000000..fa7f86c --- /dev/null +++ b/src/main/omr/lag/BasicRoi.java @@ -0,0 +1,209 @@ +//----------------------------------------------------------------------------// +// // +// B a s i c R o i // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.lag; + +import omr.glyph.Glyphs; +import omr.glyph.facets.Glyph; + +import omr.math.Histogram; + +import omr.run.Orientation; +import omr.run.Run; +import omr.run.RunsTable; + +import java.awt.Rectangle; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +/** + * Class {@code BasicRoi} implements an Roi + * + * @author Hervé Bitteur + */ +public class BasicRoi + implements Roi +{ + //~ Instance fields -------------------------------------------------------- + + /** Region of interest with absolute coordinates */ + final Rectangle absContour; + + //~ Constructors ----------------------------------------------------------- + //----------// + // BasicRoi // + //----------// + /** + * Define a region of interest + * + * @param absoluteContour the absolute contour of the region of interest, + * specified in the usual (x, y, width, height) form. + */ + public BasicRoi (Rectangle absoluteContour) + { + this.absContour = absoluteContour; + } + + //~ Methods ---------------------------------------------------------------- + //--------------------// + // getAbsoluteContour // + //--------------------// + @Override + public Rectangle getAbsoluteContour () + { + return new Rectangle(absContour); + } + + //-------------------// + // getGlyphHistogram // + //-------------------// + @Override + public Histogram getGlyphHistogram (Orientation projection, + Collection glyphs) + { + return getSectionHistogram( + projection, + Glyphs.sectionsOf(glyphs)); + } + + //-----------------// + // getRunHistogram // + //-----------------// + @Override + public Histogram getRunHistogram (Orientation projection, + RunsTable table) + { + final Orientation tableOrient = table.getOrientation(); + final boolean alongTheRuns = projection == tableOrient; + final Histogram histo = new Histogram<>(); + final Rectangle tableContour = new Rectangle( + table.getDimension()); + final Rectangle inter = new Rectangle( + absContour.intersection(tableContour)); + final Rectangle oriInter = tableOrient.oriented(inter); + final int minPos = oriInter.y; + final int maxPos = (oriInter.y + oriInter.height) - 1; + final int minCoord = oriInter.x; + final int maxCoord = (oriInter.x + oriInter.width) - 1; + + for (int pos = minPos; pos <= maxPos; pos++) { + List seq = table.getSequence(pos); + + for (Run run : seq) { + final int cMin = Math.max(minCoord, run.getStart()); + final int cMax = Math.min(maxCoord, run.getStop()); + + // Clipping on coord + if (cMin <= cMax) { + if (alongTheRuns) { + // Along the runs + histo.increaseCount(pos, cMax - cMin + 1); + } else { + // Across the runs + for (int i = cMin; i <= cMax; i++) { + histo.increaseCount(i, 1); + } + } + } + } + } + + return histo; + } + + //---------------------// + // getSectionHistogram // + //---------------------// + @Override + public Histogram getSectionHistogram (Orientation projection, + Collection
sections) + { + // Split the sections into 2 populations along & across wrt projection + List
along = new ArrayList<>(); + List
across = new ArrayList<>(); + + for (Section section : sections) { + if (section.isVertical() == projection.isVertical()) { + along.add(section); + } else { + across.add(section); + } + } + + final Histogram histo = new Histogram<>(); + populate(histo, projection, along, true); + populate(histo, projection.opposite(), across, false); + + return histo; + } + + //----------// + // toString // + //----------// + @Override + public String toString () + { + return "Roi " + getAbsoluteContour(); + } + + //----------// + // populate // + //----------// + /** + * Populate an histo with a collection of sections + * + * @param histo the histo to populate + * @param sectionOrientation orientation of the sections + * @param sections the collections of (parallel) sections + * @param alongTheRuns true if sections are parallel to projection + */ + private void populate (Histogram histo, + Orientation sectionOrientation, + List
sections, + boolean alongTheRuns) + { + final Rectangle oriContour = sectionOrientation.oriented(absContour); + final int minPos = oriContour.y; + final int maxPos = (oriContour.y + oriContour.height) - 1; + final int minCoord = oriContour.x; + final int maxCoord = (oriContour.x + oriContour.width) - 1; + + for (Section section : sections) { + int pos = section.getFirstPos() - 1; + + for (Run run : section.getRuns()) { + pos++; + + // Clipping on pos + if ((pos < minPos) || (pos > maxPos)) { + continue; + } + + final int cMin = Math.max(minCoord, run.getStart()); + final int cMax = Math.min(maxCoord, run.getStop()); + + // Clipping on coord + if (cMin <= cMax) { + if (alongTheRuns) { + // Along the runs + histo.increaseCount(pos, cMax - cMin + 1); + } else { + // Across the runs + for (int i = cMin; i <= cMax; i++) { + histo.increaseCount(i, 1); + } + } + } + } + } + } +} diff --git a/src/main/omr/lag/BasicSection.java b/src/main/omr/lag/BasicSection.java new file mode 100644 index 0000000..ef1c1a6 --- /dev/null +++ b/src/main/omr/lag/BasicSection.java @@ -0,0 +1,1719 @@ +//----------------------------------------------------------------------------// +// // +// B a s i c S e c t i o n // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.lag; + +import omr.glyph.Nest; +import omr.glyph.Shape; +import omr.glyph.facets.Glyph; + +import omr.graph.BasicVertex; + +import omr.math.Barycenter; +import omr.math.BasicLine; +import omr.math.Line; +import omr.math.PointsCollector; + +import omr.run.Orientation; +import omr.run.Run; + +import omr.sheet.SystemInfo; + +import omr.stick.SectionRole; +import omr.stick.StickRelation; + +import omr.ui.Colors; + +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.Polygon; +import java.awt.Rectangle; +import java.awt.Stroke; +import java.awt.geom.PathIterator; +import java.awt.image.BufferedImage; +import java.awt.image.WritableRaster; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.ListIterator; +import java.util.Set; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlAttribute; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlRootElement; +import javax.xml.bind.annotation.adapters.XmlAdapter; +import omr.ui.util.UIUtil; + +/** + * Class {@code BasicSection} is a basic implementation of {@link Section}. + * + *

TODO: Get rid of StickRelation part ASAP? + * + * @author Hervé Bitteur + */ +@XmlAccessorType(XmlAccessType.NONE) +@XmlRootElement(name = "section") +public class BasicSection + extends BasicVertex + implements Section +{ + //~ Static fields/initializers --------------------------------------------- + + /** Usual logger utility */ + private static final Logger logger = LoggerFactory.getLogger(BasicSection.class); + + //~ Instance fields -------------------------------------------------------- + /** Position of first run */ + @XmlAttribute(name = "first-pos") + private int firstPos; + + /** Section orientation */ + @XmlAttribute(name = "orientation") + private Orientation orientation; + + /** The collection of runs that make up the section */ + @XmlElement(name = "run") + private final List runs = new ArrayList<>(); + + /** Oriented bounding rectangle */ + protected Rectangle orientedBounds; + + /** Absolute mass center */ + private Point centroid; + + /** Contribution to the foreground */ + private int foreWeight; + + /** Length of longest run */ + private int maxRunLength; + + /** Number of pixels, whatever the gray level */ + private int weight; + + /** Absolute contour points */ + private Polygon polygon; + + /** Absolute contour box */ + private Rectangle bounds; + + /** Adjacent sections from the other orientation */ + private Set

oppositeSections; + + /** + * Glyph this section belongs to. + * This reference is kept in sync with the containing GlyphLag activeMap. + * Don't directly assign a value to 'glyph', use the setGlyph() method + * instead. + */ + private Glyph glyph; + + /** To flag sections too thick for staff line (null = don't know) */ + private Boolean fat = null; + + /** Flag to remember processing has been done */ + private boolean processed = false; + + /** (Debug) flag this section as VIP */ + private boolean vip; + + /** Relation between section and stick */ + protected StickRelation relation; + + /** Approximating oriented line for this section */ + protected Line orientedLine; + + /** The containing system, if any */ + private SystemInfo system; + + /** + * Default color. This is the permanent default which is used when + * the color is reset by {@link #resetColor} + */ + protected Color defaultColor; + + /** + * Color currently used. + * By default, the color is the defaultColor chosen out of the palette. + * But, temporarily, a section can be assigned a different color, + * for example to highlight the section. + */ + protected Color color; + + //~ Constructors ----------------------------------------------------------- + //--------------// + // BasicSection // + //--------------// + /** + * Creates a new BasicSection. + */ + public BasicSection () + { + } + + //~ Methods ---------------------------------------------------------------- + //---------------// + // allocateTable // + //---------------// + /** + * For basic print out, allocate a drawing table, to be later filled + * with section pixels + * + * @param box the limits of the drawing table + * @return the table ready to be filled + */ + public static char[][] allocateTable (Rectangle box) + { + char[][] table = new char[box.height + 1][box.width + 1]; + + for (int i = 0; i < table.length; i++) { + Arrays.fill(table[i], ' '); + } + + return table; + } + + //----------------// + // drawingOfTable // + //----------------// + /** + * Printout the filled drawing table + * + * @param table the filled table + * @param box the table limits in the image + */ + public static String drawingOfTable (char[][] table, + Rectangle box) + { + StringBuilder sb = new StringBuilder(); + sb.append(String.format("%n")); + + sb.append(String.format( + "xMin=%d, xMax=%d%n", box.x, box.x + box.width - 1)); + sb.append(String.format( + "yMin=%d, yMax=%d%n", box.y, box.y + box.height - 1)); + + for (int iy = 0; iy < table.length; iy++) { + sb.append(String.format("%d:", iy + box.y)); + char[] line = table[iy]; + for (int ix = 0; ix < line.length; ix++) { + sb.append(line[ix]); + } + sb.append(String.format("%n")); + } + + return sb.toString(); + } + + //--------------------// + // addOppositeSection // + //--------------------// + @Override + public void addOppositeSection (Section otherSection) + { + if (oppositeSections == null) { + oppositeSections = new HashSet<>(); + } + + oppositeSections.add(otherSection); + } + + //--------// + // append // + //--------// + @Override + public void append (Run run) + { + runs.add(run); + addRun(run); + + logger.debug("Appended {} to {}", run, this); + } + + //-----------// + // compareTo // + //-----------// + /** + * Needed to implement Comparable, sorting sections first by absolute + * abscissa, then by absolute ordinate. + * + * @param other the other section to compare to + * @return the result of ordering + */ + @Override + public int compareTo (Section other) + { + if (this == other) { + return 0; + } + + final Point ref = this.getBounds().getLocation(); + final Point otherRef = other.getBounds().getLocation(); + + // Are x values different? + final int dx = ref.x - otherRef.x; + + if (dx != 0) { + return dx; + } + + // Vertically aligned, so use ordinates + final int dy = ref.y - otherRef.y; + + if (dy != 0) { + return dy; + } + + // Finally, use id. Note this should return zero since different + // sections cannot overlap + return this.getId() - other.getId(); + } + + //-------------------// + // computeParameters // + //-------------------// + @Override + public void computeParameters () + { + // weight & foreWeight & maxRunLength + weight = 0; + foreWeight = 0; + maxRunLength = 0; + + // maxRunLength + for (Run run : runs) { + computeRunContribution(run); + } + + // Invalidate cached data + invalidateCache(); + + logger.debug("Parameters of {} maxRunLength={} meanRunLength={}" + + " weight={} foreWeight={}", + this, getMaxRunLength(), getMeanRunLength(), + weight, foreWeight); + } + + //----------// + // contains // + //----------// + @Override + public boolean contains (int x, + int y) + { + return getPolygon().contains(x, y); + } + + //----------// + // cumulate // + //----------// + @Override + public void cumulate (Barycenter barycenter, + Rectangle absRoi) + { + if (barycenter == null) { + throw new IllegalArgumentException("Barycenter is null"); + } + + if (absRoi == null) { + // Take all run pixels + int pos = firstPos - 1; + + for (Run run : runs) { + double coord = run.getStart() + (run.getLength() / 2d); + pos++; + + if (orientation == Orientation.HORIZONTAL) { + barycenter.include(run.getLength(), coord, pos); + } else { + barycenter.include(run.getLength(), pos, coord); + } + } + } else { + Rectangle oRoi = orientation.oriented(absRoi); + + // Take only the pixels contained by the oriented roi + int pos = firstPos - 1; + int posMax = Math.min(firstPos + runs.size(), oRoi.y + oRoi.height) + - 1; + int coordMax = (oRoi.x + oRoi.width) - 1; + + for (Run run : runs) { + pos++; + + if (pos < oRoi.y) { + continue; + } + + if (pos > posMax) { + break; + } + + final int roiStart = Math.max(run.getStart(), oRoi.x); + final int roiStop = Math.min(run.getStop(), coordMax); + + for (int coord = roiStart; coord <= roiStop; coord++) { + if (orientation == Orientation.HORIZONTAL) { + barycenter.include(coord, pos); + } else { + barycenter.include(pos, coord); + } + } + } + } + } + + //----------// + // cumulate // + //----------// + @Override + public void cumulate (PointsCollector collector) + { + final Rectangle roi = collector.getRoi(); + + if (roi == null) { + int p = firstPos; + + for (Run run : runs) { + final int start = run.getStart(); + + for (int ic = run.getLength() - 1; ic >= 0; ic--) { + if (orientation == Orientation.HORIZONTAL) { + collector.include(start + ic, p); + } else { + collector.include(p, start + ic); + } + } + + p++; + } + } else { + // Take only the pixels contained by the absolute roi + Rectangle oRoi = orientation.oriented(roi); + final int pMin = oRoi.y; + final int pMax = -1 + + Math.min( + firstPos + runs.size(), + oRoi.y + oRoi.height); + final int cMin = oRoi.x; + final int cMax = (oRoi.x + oRoi.width) - 1; + int p = firstPos - 1; + + for (Run run : runs) { + p++; + + if (p < pMin) { + continue; + } + + if (p > pMax) { + break; + } + + final int roiStart = Math.max(run.getStart(), cMin); + final int roiStop = Math.min(run.getStop(), cMax); + final int length = roiStop - roiStart + 1; + + if (length > 0) { + for (int c = roiStart; c <= roiStop; c++) { + if (orientation == Orientation.HORIZONTAL) { + collector.include(c, p); + } else { + collector.include(p, c); + } + } + } + } + } + } + + //-----------// + // drawAscii // + //-----------// + @Override + public void drawAscii () + { + System.out.println("Section#" + getId()); + + // Determine the absolute bounds + Rectangle box = getBounds(); + + char[][] table = allocateTable(box); + fillTable(table, box); + drawingOfTable(table, box); + } + + //-----------// + // fillImage // + //-----------// + @Override + public void fillImage (BufferedImage im, + Rectangle box) + { + final WritableRaster raster = im.getRaster(); + + if (isVertical()) { + int x = getFirstPos() - box.x; + + for (Run run : runs) { + for (int y = run.getStart(); y <= run.getStop(); y++) { + raster.setSample(x, y - box.y, 0, 255); + } + + x += 1; + } + } else { + int y = getFirstPos() - box.y; + + for (Run run : runs) { + for (int x = run.getStart(); x <= run.getStop(); x++) { + raster.setSample(x - box.x, y, 0, 255); + } + + y += 1; + } + } + } + + //-----------// + // fillTable // + //-----------// + @Override + public void fillTable (char[][] table, + Rectangle box) + { + // Determine the bounds + getPolygon(); // Make sure the polygon is available + + int xPrev = 0; + int yPrev = 0; + int x; + int y; + + for (int i = 0; i <= polygon.npoints; i++) { + if (i == polygon.npoints) { // Last point + x = polygon.xpoints[0] - box.x; + y = polygon.ypoints[0] - box.y; + } else { + x = polygon.xpoints[i] - box.x; + y = polygon.ypoints[i] - box.y; + } + + if (i > 0) { + if (x != xPrev) { // Horizontal + + int x1 = Math.min(x, xPrev); + int x2 = Math.max(x, xPrev); + + for (int ix = x1 + 1; ix < x2; ix++) { + table[y][ix] = '-'; + } + } else { // Vertical + + int y1 = Math.min(y, yPrev); + int y2 = Math.max(y, yPrev); + + for (int iy = y1 + 1; iy < y2; iy++) { + table[iy][x] = '|'; + } + } + } + + table[y][x] = '+'; + xPrev = x; + yPrev = y; + } + } + + //-----------------// + // getAbsoluteLine // + //-----------------// + @Override + public Line getAbsoluteLine () + { + getOrientedLine(); + + return orientation.switchRef(orientedLine); + } + + //---------------// + // getAreaCenter // + //---------------// + @Override + public Point getAreaCenter () + { + Rectangle box = getBounds(); + + return new Point( + box.x + (box.width / 2), + box.y + (box.height / 2)); + } + + //-----------// + // getAspect // + //-----------// + @Override + public double getAspect (Orientation orientation) + { + return (double) getLength(orientation) / (double) getThickness( + orientation); + } + + //-----------// + // getBounds // + //-----------// + @Override + public Rectangle getBounds () + { + if (bounds == null) { + bounds = new Rectangle(getPolygon().getBounds()); + } + + return new Rectangle(bounds); // Copy! + } + + //-------------// + // getCentroid // + //-------------// + @Override + public Point getCentroid () + { + if (centroid == null) { + Point orientedPoint = new Point(0, 0); + int y = firstPos; + + for (Run run : runs) { + final int length = run.getLength(); + orientedPoint.y += (length * (2 * y)); + orientedPoint.x += (length * ((2 * run.getStart()) + length)); + y++; + } + + orientedPoint.x /= (2 * getWeight()); + orientedPoint.y /= (2 * getWeight()); + + centroid = orientation.absolute(orientedPoint); + logger.debug("Centroid of {} is {}", this, centroid); + } + + return centroid; + } + + //-----------------// + // getDefaultColor // + //-----------------// + @Override + public Color getDefaultColor () + { + return defaultColor; + } + + //-------------------// + // getFirstAdjacency // + //-------------------// + @Override + public double getFirstAdjacency () + { + Run run = getFirstRun(); + int runStart = run.getStart(); + int runStop = run.getStop(); + int adjacency = 0; + + for (Section source : getSources()) { + Run lastRun = source.getLastRun(); + int start = Math.max(runStart, lastRun.getStart()); + int stop = Math.min(runStop, lastRun.getStop()); + + if (stop >= start) { + adjacency += (stop - start + 1); + } + } + + return (double) adjacency / (double) run.getLength(); + } + + //-------------// + // getFirstPos // + //-------------// + @Override + public int getFirstPos () + { + return firstPos; + } + + //-------------// + // getFirstRun // + //-------------// + @Override + public Run getFirstRun () + { + return runs.get(0); + } + + //---------------// + // getForeWeight // + //---------------// + @Override + public int getForeWeight () + { + return foreWeight; + } + + //----------// + // getGlyph // + //----------// + @Override + public Glyph getGlyph () + { + return glyph; + } + + //----------// + // getGraph // + //----------// + /** + * Report the containing graph (lag) of this vertex (section) + * + * @return the containing graph + */ + @Override + public Lag getGraph () + { + return graph; + } + + //------------------// + // getLastAdjacency // + //------------------// + @Override + public double getLastAdjacency () + { + Run run = getLastRun(); + int runStart = run.getStart(); + int runStop = run.getStop(); + int adjacency = 0; + + for (Section target : getTargets()) { + Run firstRun = target.getFirstRun(); + int start = Math.max(runStart, firstRun.getStart()); + int stop = Math.min(runStop, firstRun.getStop()); + + if (stop >= start) { + adjacency += (stop - start + 1); + } + } + + return (double) adjacency / (double) run.getLength(); + } + + //------------// + // getLastPos // + //------------// + @Override + public int getLastPos () + { + return (firstPos + getRunCount()) - 1; + } + + //------------// + // getLastRun // + //------------// + @Override + public Run getLastRun () + { + return runs.get(runs.size() - 1); + } + + //-----------// + // getLength // + //-----------// + @Override + public int getLength (Orientation orientation) + { + if (orientation == Orientation.HORIZONTAL) { + return getBounds().width; + } else { + return getBounds().height; + } + } + + //----------// + // getLevel // + //----------// + @Override + public int getLevel () + { + return (int) Math.rint((double) foreWeight / (double) weight); + } + + //-----------------// + // getMaxRunLength // + //-----------------// + @Override + public int getMaxRunLength () + { + return maxRunLength; + } + + //---------------// + // getMeanAspect // + //---------------// + @Override + public double getMeanAspect (Orientation orientation) + { + return getLength(orientation) / getMeanThickness(orientation); + } + + //------------------// + // getMeanRunLength // + //------------------// + @Override + public int getMeanRunLength () + { + return weight / getRunCount(); + } + + //------------------// + // getMeanThickness // + //------------------// + @Override + public double getMeanThickness (Orientation orientation) + { + return (double) getWeight() / getLength(orientation); + } + + //---------------------// + // getOppositeSections // + //---------------------// + @Override + public Set
getOppositeSections () + { + if (oppositeSections != null) { + return Collections.unmodifiableSet(oppositeSections); + } else { + return Collections.emptySet(); + } + } + + //----------------// + // getOrientation // + //----------------// + @Override + public Orientation getOrientation () + { + return orientation; + } + + //-------------------// + // getOrientedBounds // + //-------------------// + @Override + public Rectangle getOrientedBounds () + { + if (orientedBounds == null) { + orientedBounds = new Rectangle(orientation.oriented(getBounds())); + } + + return orientedBounds; + } + + //-----------------// + // getOrientedLine // + //-----------------// + @Override + public Line getOrientedLine () + { + if (orientedLine == null) { + // Compute the section line + orientedLine = new BasicLine(); + + int y = getFirstPos(); + + for (Run run : getRuns()) { + int stop = run.getStop(); + + for (int x = run.getStart(); x <= stop; x++) { + orientedLine.includePoint((double) x, (double) y); + } + + y++; + } + } + + return orientedLine; + } + + //-----------------// + // getPathIterator // + //-----------------// + @Override + public PathIterator getPathIterator () + { + return getPolygon().getPathIterator(null); + } + + //------------// + // getPolygon // + //------------// + @Override + public Polygon getPolygon () + { + if (polygon == null) { + polygon = computePolygon(); + } + + return polygon; + } + + //----------------------// + // getRectangleCentroid // + //----------------------// + @Override + public Point getRectangleCentroid (Rectangle absRoi) + { + if (absRoi == null) { + throw new IllegalArgumentException("Rectangle of Interest is null"); + } + + Barycenter barycenter = new Barycenter(); + cumulate(barycenter, absRoi); + + if (barycenter.getWeight() != 0) { + return new Point( + (int) Math.rint(barycenter.getX()), + (int) Math.rint(barycenter.getY())); + } else { + return null; + } + } + + //-------------// + // getRelation // + //-------------// + @Override + public StickRelation getRelation () + { + return relation; + } + + //-------------// + // getRunCount // + //-------------// + @Override + public int getRunCount () + { + return runs.size(); + } + + //---------// + // getRuns // + //---------// + @Override + public List getRuns () + { + return runs; + } + + //---------------// + // getStartCoord // + //---------------// + @Override + public int getStartCoord () + { + return getOrientedBounds().x; + } + + //--------------// + // getStopCoord // + //--------------// + @Override + public int getStopCoord () + { + Rectangle bounds = getOrientedBounds(); + + return bounds.x + (bounds.width - 1); + } + + //-----------// + // getSystem // + //-----------// + @Override + public SystemInfo getSystem () + { + return system; + } + + //--------------// + // getThickness // + //--------------// + @Override + public int getThickness (Orientation orientation) + { + if (orientation == Orientation.HORIZONTAL) { + return getBounds().height; + } else { + return getBounds().width; + } + } + + //-----------// + // getWeight // + //-----------// + @Override + public int getWeight () + { + if (weight == 0) { + computeParameters(); + } + + return weight; + } + + //---------------// + // inNextSibling // + //---------------// + @Override + public Section inNextSibling () + { + // Check we have sources + if (getInDegree() == 0) { + return null; + } + + // Proper source section + Section source = getSources().get(getInDegree() - 1); + + // Browse till we get to this as target + for (Iterator
li = source.getTargets().iterator(); li.hasNext();) { + Section section = li.next(); + + if (section == this) { + if (li.hasNext()) { + return li.next(); + } else { + return null; + } + } + } + + logger.error("inNextSibling inconsistent graph"); + + return null; + } + + //-------------------// + // inPreviousSibling // + //-------------------// + @Override + public Section inPreviousSibling () + { + if (getInDegree() == 0) { + return null; + } + + // Proper source section + Section source = getSources().get(0); + + // Browse till we get to this as target + for (ListIterator
li = source.getTargets().listIterator( + source.getOutDegree()); li.hasPrevious();) { + Section section = li.previous(); + + if (section == this) { + if (li.hasPrevious()) { + return li.previous(); + } else { + return null; + } + } + } + + logger.error("inPreviousSibling inconsistent graph"); + + return null; + } + + //------------// + // intersects // + //------------// + @Override + public boolean intersects (Rectangle rect) + { + return getPolygon().intersects(rect); + } + + //--------------// + // isAggregable // + //--------------// + @Override + public boolean isAggregable () + { + if ((relation == null) || !relation.isCandidate()) { + return false; + } + + return !isKnown(); + } + + //-------------// + // isColorized // + //-------------// + @Override + public boolean isColorized () + { + return defaultColor != null; + } + + //-------// + // isFat // + //-------// + @Override + public Boolean isFat () + { + return fat; + } + + //---------------// + // isGlyphMember // + //---------------// + @Override + public boolean isGlyphMember () + { + return glyph != null; + } + + //---------// + // isKnown // + //---------// + @Override + public boolean isKnown () + { + return (glyph != null) + && (glyph.isSuccessful() || glyph.isWellKnown()); + } + + //-------------// + // isProcessed // + //-------------// + @Override + public boolean isProcessed () + { + return processed; + } + + //------------// + // isVertical // + //------------// + @Override + public boolean isVertical () + { + return orientation == Orientation.VERTICAL; + } + + //-------// + // isVip // + //-------// + @Override + public boolean isVip () + { + return vip; + } + + //-------// + // merge // + //-------// + @Override + public void merge (Section other) + { + logger.debug("Merging {} with {}", this, other); + + runs.addAll(other.getRuns()); + computeParameters(); + + logger.debug("Merged {}", this); + } + + //----------------// + // outNextSibling // + //----------------// + @Override + public Section outNextSibling () + { + if (getOutDegree() == 0) { + return null; + } + + // Proper target section + Section target = getTargets().get(getOutDegree() - 1); + + // Browse till we get to this as source + for (Iterator
li = target.getSources().iterator(); li.hasNext();) { + Section section = li.next(); + + if (section == this) { + if (li.hasNext()) { + return li.next(); + } else { + return null; + } + } + } + + logger.error("outNextSibling inconsistent graph"); + + return null; + } + + //--------------------// + // outPreviousSibling // + //--------------------// + @Override + public Section outPreviousSibling () + { + if (getOutDegree() == 0) { + return null; + } + + // Proper target section + Section target = getTargets().get(getOutDegree() - 1); + + // Browse till we get to this as source + for (ListIterator
li = target.getSources().listIterator( + target.getInDegree()); li.hasPrevious();) { + Section section = li.previous(); + + if (section == this) { + if (li.hasPrevious()) { + return li.previous(); + } else { + return null; + } + } + } + + logger.error("outPreviousSibling inconsistent graph"); + + return null; + } + + //---------// + // prepend // + //---------// + @Override + public void prepend (Run run) + { + logger.debug("Prepending {} to {}", run, this); + + firstPos--; + runs.add(0, run); + addRun(run); + + logger.debug("Prepended {}", this); + } + + //--------// + // render // + //--------// + @Override + public boolean render (Graphics g, + boolean drawBorders) + { + Rectangle clip = g.getClipBounds(); + Rectangle rect = getBounds(); + Color oldColor = g.getColor(); + + if (clip.intersects(rect)) { + // Default section color + Color color = isVertical() ? Colors.GRID_VERTICAL + : Colors.GRID_HORIZONTAL; + + // Use color defined for section glyph shape, if any + Glyph glyph = getGlyph(); + + if (glyph != null) { + Shape shape = glyph.getShape(); + + if (shape != null) { + color = shape.getColor(); + } + } + + g.setColor(color); + + // Fill polygon with proper color + Polygon polygon = getPolygon(); + g.fillPolygon(polygon.xpoints, polygon.ypoints, polygon.npoints); + + // Draw polygon borders if so desired + if (drawBorders) { + g.setColor(Color.black); + g.drawPolygon( + polygon.xpoints, + polygon.ypoints, + polygon.npoints); + } + + g.setColor(oldColor); + + return true; + } else { + return false; + } + } + + //----------------// + // renderSelected // + //----------------// + @Override + public boolean renderSelected (Graphics g) + { + Rectangle clip = g.getClipBounds(); + Rectangle rect = getBounds(); + + if (clip.intersects(rect)) { + Graphics2D g2 = (Graphics2D) g; + final Stroke oldStroke = UIUtil.setAbsoluteStroke(g2, 1f); + Polygon polygon = getPolygon(); + g.setColor(Color.white); + g.fillPolygon(polygon.xpoints, polygon.ypoints, polygon.npoints); + g.setColor(Color.black); + g.drawPolygon(polygon.xpoints, polygon.ypoints, polygon.npoints); + g2.setStroke(oldStroke); + + return true; + } else { + return false; + } + } + + //------------// + // resetColor // + //------------// + @Override + public void resetColor () + { + setColor(defaultColor); + } + + //----------// + // resetFat // + //----------// + @Override + public void resetFat () + { + this.fat = null; + } + + //----------// + // setColor // + //----------// + @Override + public void setColor (Color color) + { + this.color = color; + } + + //-----------------// + // setDefaultColor // + //-----------------// + @Override + public void setDefaultColor (Color color) + { + defaultColor = color; + } + + //--------// + // setFat // + //--------// + @Override + public void setFat (boolean fat) + { + this.fat = fat; + } + + //-------------// + // setFirstPos // + //-------------// + @Override + public void setFirstPos (int firstPos) + { + this.firstPos = firstPos; + } + + //----------// + // setGlyph // + //----------// + @Override + public void setGlyph (Glyph glyph) + { + // Keep the activeMap of the containing Nest in sync! + Nest nest = null; + + if ((glyph != null) && (glyph.getNest() != null)) { + nest = glyph.getNest(); + } else if ((this.glyph != null) && (this.glyph.getNest() != null)) { + nest = this.glyph.getNest(); + } + + this.glyph = glyph; + + if (nest != null) { + nest.mapSection(this, glyph); + } + + if (isVip()) { + logger.info("{} linkedTo {}", this, glyph); + + if (glyph != null) { + glyph.setVip(); + } + } + } + + //----------// + // setGraph // + //----------// + /** + * (package access from graph) + */ + @Override + public void setGraph (Lag lag) + { + super.setGraph(lag); + + if (lag != null) { + orientation = lag.getOrientation(); + } + } + + //-----------// + // setParams // + //-----------// + /** + * Assign major parameters (kind, layer and direction), since the enclosing + * stick may be assigned later. + * + * @param role the role of this section in stick elaboration + * @param layer the layer from stick core + * @param direction the direction when departing from the stick core + */ + public void setParams (SectionRole role, + int layer, + int direction) + { + if (relation == null) { + relation = new StickRelation(); + } + + relation.setParams(role, layer, direction); + } + + //--------------// + // setProcessed // + //--------------// + @Override + public void setProcessed (boolean processed) + { + this.processed = processed; + } + + //-----------// + // setSystem // + //-----------// + @Override + public void setSystem (SystemInfo system) + { + this.system = system; + } + + //--------// + // setVip // + //--------// + @Override + public void setVip () + { + vip = true; + } + + //----------// + // toString // + //----------// + @Override + public String toString () + { + StringBuilder sb = new StringBuilder(); + + sb.append("{Section"); + + if (orientation != null) { + sb.append(isVertical() ? "V" : "H"); + } else { + sb.append("?"); + } + + sb.append("#").append(getId()); + + sb.append(internalsString()); + + sb.append("}"); + + return sb.toString(); + } + + //-----------// + // translate // + //-----------// + @Override + public void translate (Point vector) + { + // Get the coord/pos equivalent of dx/dy vector + Point cp = orientation.oriented(vector); + int dc = cp.x; + int dp = cp.y; + + // Apply the needed modifications + firstPos += dp; + + for (Run run : runs) { + run.translate(dc); + } + + // Force update + invalidateCache(); + } + + //----------------// + // computePolygon // + //----------------// + /** + * Compute the arrays of points needed to draw the section runs. + * This is an absolute definition. + */ + protected Polygon computePolygon () + { + final int maxNb = 1 + (4 * getRunCount()); // Upper value + final int[] xx = new int[maxNb]; + final int[] yy = new int[maxNb]; + int idx = 0; // Current filling index in xx & yy arrays + + if (isVertical()) { + idx = populatePolygon(yy, xx, idx, 1); + idx = populatePolygon(yy, xx, idx, -1); + } else { + idx = populatePolygon(xx, yy, idx, 1); + idx = populatePolygon(xx, yy, idx, -1); + } + + Polygon poly = new Polygon(xx, yy, idx); + + return poly; + } + + //-----------------// + // internalsString // + //-----------------// + @Override + protected String internalsString () + { + StringBuilder sb = new StringBuilder(super.internalsString()); + + if (oppositeSections != null) { + sb.append("/").append(oppositeSections.size()); + } + + // sb.append(" fPos=") + // .append(firstPos) + // .append(" "); + // sb.append(getFirstRun()); + // + // if (getRunCount() > 1) { + // sb.append("-") + // .append(getRunCount()) + // .append("-") + // .append(getLastRun()); + // } + // + // sb.append(" Wt=") + // .append(weight); + + // sb.append(" lv=") + // .append(getLevel()); + // sb.append(" fW=") + // .append(foreWeight); + if ((isFat() != null) && isFat()) { + sb.append(" fat"); + } + + if (relation != null) { + sb.append(" ").append(relation); + } + + if (glyph != null) { + sb.append(" ").append(glyph.idString()); + + if (glyph.getShape() != null) { + sb.append(":").append(glyph.getShape()); + } + } + + // if (system != null) { + // sb.append(" syst:") + // .append(system.getId()); + // } + return sb.toString(); + } + + //-----------------// + // invalidateCache // + //-----------------// + protected void invalidateCache () + { + orientedBounds = null; + centroid = null; + polygon = null; + bounds = null; + orientedLine = null; + } + + //--------// + // addRun // + //--------// + /** + * Compute incrementally the cached parameters + */ + private void addRun (Run run) + { + // Invalidate cached data + invalidateCache(); + + // Link back from run to section + run.setSection(this); + + // Compute contribution of this run + computeRunContribution(run); + } + + //------------------------// + // computeRunContribution // + //------------------------// + private void computeRunContribution (Run run) + { + final int length = run.getLength(); + weight += length; + foreWeight += (length * run.getLevel()); + maxRunLength = Math.max(maxRunLength, length); + } + + //-----------------// + // populatePolygon // + //-----------------// + /** + * Compute the arrays of points needed to draw the section runs + * + * @param xpoints to receive abscissae + * @param ypoints to receive coordinates + * @param dir direction for browsing runs + * @param index first index available in arrays + * @return last index value + */ + private int populatePolygon (int[] xpoints, + int[] ypoints, + int index, + int dir) + { + // Precise delimitating points + int runNb = getRunCount(); + int iStart = (dir > 0) ? 0 : (runNb - 1); + int iBreak = (dir > 0) ? runNb : (-1); + int y = (dir > 0) ? getFirstPos() : (getFirstPos() + runNb); + int xPrev = -1; + + for (int i = iStart; i != iBreak; i += dir) { + Run run = runs.get(i); + + // +----------------------------+ + // +--+-------------------------+ + // +----------------------+--+ + // +----------------------+ + // + // Order of the 4 angle points for a run is + // Vertical lag: Horizontal lag: + // 1 2 1 4 + // 4 3 2 3 + int x = (dir > 0) ? run.getStart() : (run.getStop() + 1); + + if (x != xPrev) { + if (xPrev != -1) { + // Insert last vertex + xpoints[index] = xPrev; + ypoints[index] = y; + index++; + } + + // Insert new vertex + xpoints[index] = x; + ypoints[index] = y; + index++; + xPrev = x; + } + + y += dir; + } + + // Complete the sequence, with a new vertex + xpoints[index] = xPrev; + ypoints[index] = y; + index++; + + if (dir < 0) { + // Finish with starting point + xpoints[index] = runs.get(0).getStart(); + ypoints[index] = getFirstPos(); + index++; + } + + return index; + } + + //~ Inner Classes ---------------------------------------------------------- + //---------// + // Adapter // + //---------// + /** + * Meant for JAXB handling of Section interface + */ + public static class Adapter + extends XmlAdapter + { + //~ Methods ------------------------------------------------------------ + + @Override + public BasicSection marshal (Section s) + { + return (BasicSection) s; + } + + @Override + public Section unmarshal (BasicSection s) + { + return s; + } + } +} diff --git a/src/main/omr/lag/JunctionAllPolicy.java b/src/main/omr/lag/JunctionAllPolicy.java new file mode 100644 index 0000000..b272b32 --- /dev/null +++ b/src/main/omr/lag/JunctionAllPolicy.java @@ -0,0 +1,65 @@ +//----------------------------------------------------------------------------// +// // +// J u n c t i o n A l l P o l i c y // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.lag; + +import omr.run.Run; + +/** + * Class {@code JunctionAllPolicy} defines a junction policy which + * imposes no condition on run consistency, thus taking all runs + * considered. + * + * @author Hervé Bitteur + */ +public class JunctionAllPolicy + implements JunctionPolicy +{ + //~ Constructors ----------------------------------------------------------- + + //-------------------// + // JunctionAllPolicy // + //-------------------// + /** + * Creates an instance of this policy. + */ + public JunctionAllPolicy () + { + } + + //~ Methods ---------------------------------------------------------------- + //---------------// + // consistentRun // + //---------------// + /** + * Check whether the Run is consistent with the provided Section, + * according to this junction policy. + * + * @param run the Run candidate + * @param section the potentially hosting Section + * @return always true + */ + @Override + public boolean consistentRun (Run run, + Section section) + { + return true; + } + + //----------// + // toString // + //----------// + @Override + public String toString () + { + return "{JunctionAllPolicy}"; + } +} diff --git a/src/main/omr/lag/JunctionDeltaPolicy.java b/src/main/omr/lag/JunctionDeltaPolicy.java new file mode 100644 index 0000000..183c708 --- /dev/null +++ b/src/main/omr/lag/JunctionDeltaPolicy.java @@ -0,0 +1,80 @@ +//----------------------------------------------------------------------------// +// // +// J u n c t i o n D e l t a P o l i c y // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.lag; + +import omr.run.Run; + +/** + * Class {@code JunctionDeltaPolicy} defines a junction policy based + * on the delta between the length of the candidate run and the length + * of the last run of the section. + * + * @author Hervé Bitteur + */ +public class JunctionDeltaPolicy + implements JunctionPolicy +{ + //~ Instance fields -------------------------------------------------------- + + /** + * Maximum value acceptable for delta length, for a delta criteria + */ + private final int maxDeltaLength; + + //~ Constructors ----------------------------------------------------------- + //---------------------// + // JunctionDeltaPolicy // + //---------------------// + /** + * Creates an instance of policy based on delta run length. + * + * @param maxDeltaLength the maximum possible length gap between two + * consecutive rows + */ + public JunctionDeltaPolicy (int maxDeltaLength) + { + this.maxDeltaLength = maxDeltaLength; + } + + //~ Methods ---------------------------------------------------------------- + //---------------// + // consistentRun // + //---------------// + /** + * Check whether the Run is consistent with the provided Section, + * according to this junction policy, based on run length and last + * section run length. + * + * @param run the Run candidate + * @param section the potentially hosting Section + * @return true if consistent, false otherwise + */ + @Override + public boolean consistentRun (Run run, + Section section) + { + // Check based on absolute differences between the two runs + Run last = section.getLastRun(); + + return Math.abs(run.getLength() - last.getLength()) <= maxDeltaLength; + } + + //----------// + // toString // + //----------// + @Override + public String toString () + { + return "{JunctionDeltaPolicy" + " maxDeltaLength=" + maxDeltaLength + + "}"; + } +} diff --git a/src/main/omr/lag/JunctionPolicy.java b/src/main/omr/lag/JunctionPolicy.java new file mode 100644 index 0000000..4230468 --- /dev/null +++ b/src/main/omr/lag/JunctionPolicy.java @@ -0,0 +1,41 @@ +//----------------------------------------------------------------------------// +// // +// J u n c t i o n P o l i c y // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.lag; + +import omr.run.Run; + +/** + * Interface {@code JunctionPolicy} encapsulates the policy that + * decides if a run can extend a given section. + * If not, the run is part of a new section, linked to the previous one by a + * junction. + * + * @author Hervé Bitteur + */ +public interface JunctionPolicy +{ + //~ Methods ---------------------------------------------------------------- + + //---------------// + // consistentRun // + //---------------// + /** + * Check if provided run is consistent with the section defined + * so far. + * + * @param run the candidate run for section extension + * @param section the to-be extended section + * @return true is extension is compatible with the defined junction policy + */ + public abstract boolean consistentRun (Run run, + Section section); +} diff --git a/src/main/omr/lag/JunctionRatioPolicy.java b/src/main/omr/lag/JunctionRatioPolicy.java new file mode 100644 index 0000000..c07983d --- /dev/null +++ b/src/main/omr/lag/JunctionRatioPolicy.java @@ -0,0 +1,87 @@ +//----------------------------------------------------------------------------// +// // +// J u n c t i o n R a t i o P o l i c y // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.lag; + +import omr.run.Run; + +/** + * Class {@code JunctionRatioPolicy} defines a junction policy based + * on the ratio between the length of the candidate run and the mean + * length of the section runs so far. + * + * @author Hervé Bitteur + */ +public class JunctionRatioPolicy + implements JunctionPolicy +{ + //~ Instance fields -------------------------------------------------------- + + /** + * Maximum value acceptable for length ratio, for a ratio criteria + */ + private final double maxLengthRatio; + + /** + * Minimum value acceptable for length ratio, for a ratio criteria + */ + private final double minLengthRatio; + + //~ Constructors ----------------------------------------------------------- + //---------------------// + // JunctionRatioPolicy // + //---------------------// + /** + * Creates a policy based on ratio of run length versus mean length + * of section runs. + * + * @param maxLengthRatio maximum difference ratio to continue the + * current section + */ + public JunctionRatioPolicy (double maxLengthRatio) + { + this.maxLengthRatio = maxLengthRatio; + this.minLengthRatio = 1f / maxLengthRatio; + } + + //~ Methods ---------------------------------------------------------------- + //---------------// + // consistentRun // + //---------------// + /** + * Check whether the Run is consistent with the provided Section, + * according to this junction policy, based on run length and mean + * section run length. + * + * @param run the Run candidate + * @param section the potentially hosting Section + * @return true if consistent, false otherwise + */ + @Override + public boolean consistentRun (Run run, + Section section) + { + // Check is based on ratio of lengths + final double ratio = (double) run.getLength() / section.getMeanRunLength(); + + return (ratio <= maxLengthRatio) && (ratio >= minLengthRatio); + } + + //----------// + // toString // + //----------// + @Override + public String toString () + { + return "{JunctionRatioPolicy" + " maxLengthRatio=" + maxLengthRatio + + " minLengthRatio=" + minLengthRatio + "}"; + } +} diff --git a/src/main/omr/lag/Lag.java b/src/main/omr/lag/Lag.java new file mode 100644 index 0000000..b680ae6 --- /dev/null +++ b/src/main/omr/lag/Lag.java @@ -0,0 +1,188 @@ +//----------------------------------------------------------------------------// +// // +// L a g // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.lag; + +import omr.graph.Digraph; + +import omr.run.Oriented; +import omr.run.Run; +import omr.run.RunsTable; + +import omr.selection.SectionEvent; +import omr.selection.SectionIdEvent; +import omr.selection.SectionSetEvent; +import omr.selection.SelectionService; + +import omr.util.Predicate; + +import java.awt.Rectangle; +import java.util.Collection; +import java.util.List; +import java.util.Set; + +/** + * Interface {@code Lag} defines a graph of {@link Section} instances + * (sets of contiguous runs with compatible lengths), linked by + * Junctions when there is no more contiguous run or when the + * compatibility is no longer met. + * + * Sections are thus vertices of the graph, while junctions are directed edges + * between sections. All the sections (and runs) have the same orientation + * shared by the lag. + * + *

A lag may have a related UI selection service accessible through {@link + * #getSectionService}. This selection service handles Section, SectionId and + * SectionSet events. The {@link #getSelectedSection} and + * {@link #getSelectedSectionSet} methods are just convenient ways to retrieve + * the last selected section, sectionId or sectionSet from the lag selection + * service.

+ * + *

Run selection is provided by a separate selection service hosted by the + * underlying RunsTable instance. For convenience, one can use the method + * {@link #getRunService()} to get access to this run service.

+ * + * @author Hervé Bitteur + */ +public interface Lag + extends Digraph, Oriented +{ + //~ Static fields/initializers --------------------------------------------- + + /** Events that can be published on lag section service */ + static final Class[] eventsWritten = new Class[]{ + SectionIdEvent.class, + SectionEvent.class, + SectionSetEvent.class + }; + + //~ Methods ---------------------------------------------------------------- + /** + * Include the content of runs table to the lag. + * + * @param runsTable the populated runs + */ + void addRuns (RunsTable runsTable); + + /** + * Create a section in the lag (using the defined vertexClass). + * + * @param firstPos the starting position of the section + * @param firstRun the very first run of the section + * @return the created section + */ + Section createSection (int firstPos, + Run firstRun); + + /** + * Cut dependency about other services for lag. + */ + void cutServices (); + + /** + * Report the run found at given coordinates, if any. + * + * @param x absolute abscissa + * @param y absolute ordinate + * @return the run found, or null otherwise + */ + Run getRunAt (int x, + int y); + + /** + * Report the selection service for runs. + * + * @return the run selection service + */ + SelectionService getRunService (); + + /** + * Report the underlying runs table. + * + * @return the table of runs + */ + RunsTable getRuns (); + + /** + * Report the section selection service. + * + * @return the section selection service + */ + SelectionService getSectionService (); + + /** + * Return a view of the collection of sections that are currently + * part of this lag. + * + * @return the sections collection + */ + Collection
getSections (); + + /** + * Convenient method to report the UI currently selected Section, + * if any, in this lag. + * + * @return the UI selected section, or null if none + */ + Section getSelectedSection (); + + /** + * Convenient method to report the UI currently selected set of + * Sections, if any, in this lag. + * + * @return the UI selected section set, or null if none + */ + Set
getSelectedSectionSet (); + + /** + * Lookup for lag sections that are intersected by the + * provided rectangle. + * Specific sections are not considered. + * + * @param rect the given rectangle + * @return the set of lag sections intersected, which may be empty + */ + Set
lookupIntersectedSections (Rectangle rect); + + /** + * Lookup for lag sections that are contained in the + * provided rectangle. + * Specific sections are not considered. + * + * @param rect the given rectangle + * @return the set of lag sections contained, which may be empty + */ + Set
lookupSections (Rectangle rect); + + /** + * Purge the lag of all sections for which provided predicate holds. + * + * @param predicate means to specify whether a section applies for purge + * @return the list of sections purged in this call + */ + List
purgeSections (Predicate
predicate); + + /** + * Use the provided runs table as the lag underlying table. + * + * @param runsTable the populated runs + */ + void setRuns (RunsTable runsTable); + + /** + * Inject dependency about other services for lag. + * + * @param locationService the location service to read & write + * @param sceneService the glyphservice to write + */ + void setServices (SelectionService locationService, + SelectionService sceneService); +} diff --git a/src/main/omr/lag/Roi.java b/src/main/omr/lag/Roi.java new file mode 100644 index 0000000..b55b81c --- /dev/null +++ b/src/main/omr/lag/Roi.java @@ -0,0 +1,77 @@ +//----------------------------------------------------------------------------// +// // +// R o i // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.lag; + +import omr.glyph.facets.Glyph; + +import omr.math.Histogram; + +import omr.run.Orientation; +import omr.run.RunsTable; + +import java.awt.Rectangle; +import java.util.Collection; + +/** + * Interface {@code Roi} defines aan absolute rectangular region of + * interest, on which histograms can be computed vertically and + * horizontally. + * + * @author Hervé Bitteur + */ +public interface Roi +{ + //~ Methods ---------------------------------------------------------------- + + /** + * Report the rectangular contour, in absolute coordinates + * + * @return the absolute contour + */ + Rectangle getAbsoluteContour (); + + /** + * Report the histogram obtained in the provided projection orientation + * of the runs contained in the provided glyphs + * + * @param projection the orientation of the projection + * @param glyphs the provided glyphs (which can contain sections of + * various + * orientations) + * @return the computed histogram + */ + Histogram getGlyphHistogram (Orientation projection, + Collection glyphs); + + /** + * Report the histogram obtained in the provided projection orientation + * of the runs contained in the provided runs table + * + * @param projection the orientation of the projection + * @param table the runs table + * @return the computed histogram + */ + Histogram getRunHistogram (Orientation projection, + RunsTable table); + + /** + * Report the histogram obtained in the provided projection orientation + * of the runs contained in the provided sections + * + * @param projection the orientation of the projection + * @param sections the provided sections (which can be of various + * orientations) + * @return the computed histogram + */ + Histogram getSectionHistogram (Orientation projection, + Collection
sections); +} diff --git a/src/main/omr/lag/Section.java b/src/main/omr/lag/Section.java new file mode 100644 index 0000000..c988f80 --- /dev/null +++ b/src/main/omr/lag/Section.java @@ -0,0 +1,601 @@ +//----------------------------------------------------------------------------// +// // +// S e c t i o n // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.lag; + +import omr.glyph.facets.Glyph; + +import omr.graph.Vertex; + +import omr.lag.ui.SectionView; + +import omr.math.Barycenter; +import omr.math.Line; +import omr.math.PointsCollector; + +import omr.run.Orientation; +import omr.run.Oriented; +import omr.run.Run; + +import omr.sheet.SystemInfo; + +import omr.stick.StickRelation; + +import omr.util.Vip; + +import java.awt.Point; +import java.awt.Polygon; +import java.awt.Rectangle; +import java.awt.geom.PathIterator; +import java.awt.image.BufferedImage; +import java.util.Comparator; +import java.util.List; +import java.util.Set; + +import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter; + +/** + * Interface {@code Section} handles a section of contiguous and + * compatible {@link Run} instances. + * + *

A section carries orientation information, which is the orientation for + * all runs in this section. + * + *

  1. Positions increase in parallel with run numbers, so the thickness + * of a section is defined as the delta between last and first positions, in + * other words its number of runs.
  2. + * + *
  3. Coordinates increase along any section run, so the section start is the + * minimum of all run starting coordinates, and the section stop is the maximum + * of all run stopping coordinates. We define section length as the value: stop + * - start +1
+ * + *

Beware, the section orientation only governs the runs orientation. + * It by no means implies that the section dimension is longer in the direction + * along the runs than in the direction across. + * To enforce this, the {@link #getLength(Orientation)} requires that an + * explicit orientation be provided, just like for {@link Glyph} instances. + * + * @author Hervé Bitteur + */ +@XmlJavaTypeAdapter(BasicSection.Adapter.class) +public interface Section + extends Vertex, Comparable

, Oriented, SectionView, Vip +{ + //~ Static fields/initializers --------------------------------------------- + + /** A section comparator, using section id */ + public static final Comparator
idComparator = new Comparator
() + { + @Override + public int compare (Section s1, + Section s2) + { + return Integer.signum(s1.getId() - s2.getId()); + } + }; + + /** For comparing Section instances on their decreasing weight */ + public static final Comparator
reverseWeightComparator = new Comparator
() + { + @Override + public int compare (Section s1, + Section s2) + { + return Integer.signum(s2.getWeight() - s1.getWeight()); + } + }; + + /** For comparing Section instances on their start value */ + public static final Comparator
startComparator = new Comparator
() + { + @Override + public int compare (Section s1, + Section s2) + { + return s1.getStartCoord() - s2.getStartCoord(); + } + }; + + /** For comparing Section instances on their pos value */ + public static final Comparator
posComparator = new Comparator
() + { + @Override + public int compare (Section s1, + Section s2) + { + return s1.getFirstPos() - s2.getFirstPos(); + } + }; + + //~ Methods ---------------------------------------------------------------- + /** + * Register the adjacency of a section from the other orientation. + * + * @param otherSection the other section to remember + */ + public void addOppositeSection (Section otherSection); + + /** + * Extend a section with the given run. + * This new run is assumed to be contiguous to the current last run of the + * section, no check is performed. + * + * @param run the new last run + */ + public void append (Run run); + + /** + * Compute the various cached parameters from scratch. + */ + public void computeParameters (); + + /** + * Predicate to check whether the given absolute point is located + * inside the section. + * + * @param x absolute abscissa + * @param y absolute ordinate + * @return true if absolute point(x,y) is contained in the section + */ + public boolean contains (int x, + int y); + + /** + * Cumulate in the provided absolute Barycenter the section pixels + * that are contained in the provided roi Rectangle. + * If the roi is null, all pixels are cumulated into the barycenter. + * + * @param barycenter the absolute point to populate + * @param absRoi the absolute rectangle of interest + */ + public void cumulate (Barycenter barycenter, + Rectangle absRoi); + + /** + * Cumulate all points that compose the runs of the section, into + * the provided absolute collector. + * + * @param collector the absolute points collector to populate + */ + public void cumulate (PointsCollector collector); + + /** + * Draws a basic representation of the section, using ascii chars. + */ + public void drawAscii (); + + /** + * Build an image with the pixels of this section. + * + * @param im the image to populate with this section + * @param box absolute bounding box (used as image coordinates reference) + */ + public void fillImage (BufferedImage im, + Rectangle box); + + /** + * Draws the section, into the provided table. + */ + public void fillTable (char[][] table, + Rectangle box); + + /** + * Return the absolute line which best approximates the + * section. + * + * @return the absolute fitted line + * @see #getOrientedLine() + */ + public Line getAbsoluteLine (); + + /** + * Report the section area absolute center. + * + * @return the area absolute center + */ + public Point getAreaCenter (); + + /** + * Report the ratio of length over thickness, along provided + * orientation. + * + * @return the "slimness" of the section + */ + public double getAspect (Orientation orientation); + + /** + * Return a COPY of the absolute bounding box. + * + * @return the absolute bounding box + */ + @Override + public Rectangle getBounds (); + + /** + * Return the absolute point which is at the mass center of the + * section, with all pixels considered of equal weight. + * + * @return the mass center of the section, as a absolute point + */ + public Point getCentroid (); + + /** + * Return the adjacency ratio on the incoming junctions. + * This is computed as the ratio to the length of the first run, of the + * sum of run overlapping lengths of the incoming junctions. In other + * words, this is a measure of how much the section at hand is + * overlapped with runs. + * + *
  • An isolated section/vertex, such as the one related to a + * barline, will exhibit a very low adjacency ratio.
  • + * + *
  • On the contrary, a section which is just a piece of a larger glyph, + * such as a treble clef or a brace, will have a higher adjacency.
  • + *
+ * + * @return the percentage of overlapped run length + * @see #getLastAdjacency + */ + public double getFirstAdjacency (); + + /** + * Return the position (x for vertical runs, y for horizontal runs) + * of the first run of the section. + * + * @return the position + */ + public int getFirstPos (); + + /** + * Return the first run within the section. + * + * @return the run, which always exists + */ + public Run getFirstRun (); + + /** + * Return the contribution of the section to the foreground. + * + * @return the section foreground weight + */ + public int getForeWeight (); + + /** + * Report the glyph the section belongs to, if any. + * + * @return the glyph, which may be null + */ + public Glyph getGlyph (); + + /** + * Return the adjacency ratio at the end of the section at hand. + * See getFirstAdjacency for explanation of the role of adjacency. + * + * @return the percentage of overlapped run length + * @see #getFirstAdjacency + */ + public double getLastAdjacency (); + + /** + * Return the position of the last run of the section. + * + * @return the position of last run + */ + public int getLastPos (); + + /** + * Return the last run of the section. + * + * @return this last run (rightmost run for vertical section) + */ + public Run getLastRun (); + + /** + * Return the length of the section, using the provided orientation. + */ + public int getLength (Orientation orientation); + + /** + * Return the mean gray level of the section. + * + * @return the section foreground level (0 -> 255) + */ + public int getLevel (); + + /** + * Return the size of the longest run in the section. + * + * @return the maximum run length + */ + public int getMaxRunLength (); + + /** + * Report the ratio of length over mean thickness, using the + * provided orientation. + * + * @return the "slimness" of the section + */ + public double getMeanAspect (Orientation orientation); + + /** + * Return the average value for all run lengths in the section. + * + * @return the mean run length + */ + public int getMeanRunLength (); + + /** + * Report the average thickness of the section, using the provided + * orientation. + * + * @return the average thickness of the section + */ + public double getMeanThickness (Orientation orientation); + + /** + * A read-only access to adjacent sections from opposite orientation + * + * @return the set of adjacent sections of the opposite orientation + */ + public Set
getOppositeSections (); + + /** + * Return the section bounding rectangle, using the runs + * orientation. + * Please clone it if you want to modify it afterwards + * + * @return the section bounding rectangle + */ + public Rectangle getOrientedBounds (); + + /** + * Report the line which best approximates the section, using the + * runs orientation. + * + * @return the oriented fitted line + * @see #getAbsoluteLine() + */ + public Line getOrientedLine (); + + /** + * Create an iterator along the absolute polygon that represents + * the section contour. + * + * @return an iterator on the underlying polygon + */ + public PathIterator getPathIterator (); + + /** + * Return the absolute polygon that defines the display contour. + * + * @return the absolute perimeter contour + */ + public Polygon getPolygon (); + + /** + * Report the absolute centroid of the section pixels found in the + * provided absolute region of interest. + * + * @param absRoi the absolute rectangle that defines the region of interest + * @return the absolute centroid + */ + public Point getRectangleCentroid (Rectangle absRoi); + + //TODO: REMOVE getRelation ASAP + public StickRelation getRelation (); + + /** + * Report the number of runs this sections contains. + * + * @return the nb of runs in the section + */ + public int getRunCount (); + + /** + * Return the list of all runs in this section. + * + * @return the section runs + */ + public List getRuns (); + + /** + * Return the smallest run starting coordinate, which is the + * smallest y value (ordinate) for a section of vertical runs. + * + * @return the starting coordinate of the section + */ + public int getStartCoord (); + + /** + * Return the largest run stopping coordinate, which is the + * largest y value (ordinate) for a section of vertical runs. + * + * @return the stopping coordinate of the section + */ + public int getStopCoord (); + + /** + * Report the containing system. + * + * @return the system (may be null) + */ + public SystemInfo getSystem (); + + /** + * Return the thickness of the section, using the provided + * orientation. + * + * @return the thickness across the provided orientation. + */ + public int getThickness (Orientation orientation); + + /** + * Return the total weight of the section, which is the sum of the + * weight (length) of all runs. + * + * @return the section weight + */ + public int getWeight (); + + /** + * Return the next sibling section, both linked by source of + * last incoming edge. + * + * @return the next sibling or null + */ + public Section inNextSibling (); + + /** + * Return the previous sibling section, both linked by source of + * first incoming edge. + * + * @return the previous sibling or null + */ + public Section inPreviousSibling (); + + /** + * Check that the section at hand is a candidate section not yet + * aggregated to a recognized stick. + * + * @return true if aggregable (but not yet aggregated) + */ + public boolean isAggregable (); + + /** + * Report whether this section is "fat", according to the current + * criteria and desired orientation. + * + * @return the fat flag, if any + */ + public Boolean isFat (); + + /** + * Checks whether the section is already a member of a glyph. + * + * @return the result of the test + */ + public boolean isGlyphMember (); + + /** + * Check that the section at hand is a member section, aggregated + * to a known glyph. + * + * @return true if member of a known glyph + */ + public boolean isKnown (); + + /** + * Report whether this section has been "processed". + * + * @return the processed + */ + public boolean isProcessed (); + + /** + * Reports whether this section is organized in vertical runs. + * + * @return true if vertical, false otherwise + */ + public boolean isVertical (); + + /** + * Merge this section with the other provided section, which is not + * affected, and must generally be destroyed. + * TODO: rename as "include". + * It is assumed (and not checked) that the two sections are + * contiguous. + * + * @param other the other section to include into this one + */ + public void merge (Section other); + + /** + * Return the next sibling section, both linked by target of + * the last outgoing edge. + * + * @return the next sibling or null + */ + public Section outNextSibling (); + + /** + * Return the previous sibling section, both linked by target of + * the first outgoing edge. + * + * @return the previous sibling or null + */ + public Section outPreviousSibling (); + + /** + * Add a run at the beginning rather than at the end of the + * section. + * + * @param run the new first run + */ + public void prepend (Run run); + + /** + * Nullify the fat sticky attribute. + */ + public void resetFat (); + + /** + * Record the current "fatness" value of this section. + * + * @param fat the fat flag + */ + public void setFat (boolean fat); + + /** + * Set the position of the first run of the section. + * + * @param firstPos position of the first run, abscissa for a vertical run, + * ordinate for a horizontal run. + */ + public void setFirstPos (int firstPos); + + /** + * Assign the containing glyph, if any. + * + * @param glyph the containing glyph, perhaps null + */ + public void setGlyph (Glyph glyph); + + /** + * Set a flag to be used at caller's will. + * + * @param processed the processed to set + */ + public void setProcessed (boolean processed); + + /** + * Assign a containing system. + * + * @param system the system to set + */ + public void setSystem (SystemInfo system); + + /** + * Apply an absolute translation vector to this section. + * + * @param vector the translation vector + */ + public void translate (Point vector); + + /** + * Predicate to check whether the given absolute rectangle is + * intersected by the section. + * + * @param rectangle absolute rectangle + * @return true if intersection is not empty + */ + boolean intersects (Rectangle rectangle); +} diff --git a/src/main/omr/lag/SectionSignature.java b/src/main/omr/lag/SectionSignature.java new file mode 100644 index 0000000..8d68b30 --- /dev/null +++ b/src/main/omr/lag/SectionSignature.java @@ -0,0 +1,106 @@ +//----------------------------------------------------------------------------// +// // +// S e c t i o n S i g n a t u r e // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.lag; + +import java.awt.Rectangle; + +/** + * Class {@code SectionSignature} defines a signature for a section + * + * @author Hervé Bitteur + */ +public class SectionSignature +{ + //~ Instance fields -------------------------------------------------------- + + /** Section weight */ + private final int weight; + + /** Section bounds */ + private Rectangle bounds; + + //~ Constructors ----------------------------------------------------------- + /** + * Creates a new SectionSignature object. + * + * @param weight the section weight + * @param bounds the section bounds + */ + public SectionSignature (int weight, + Rectangle bounds) + { + this.weight = weight; + this.bounds = bounds; + } + + //~ Methods ---------------------------------------------------------------- + //--------// + // equals // + //--------// + @Override + public boolean equals (Object obj) + { + if (obj == this) { + return true; + } + + if (obj instanceof SectionSignature) { + SectionSignature that = (SectionSignature) obj; + + return (weight == that.weight) && (bounds.x == that.bounds.x) + && (bounds.y == that.bounds.y) + && (bounds.width == that.bounds.width) + && (bounds.height == that.bounds.height); + } else { + return false; + } + } + + //----------// + // hashCode // + //----------// + @Override + public int hashCode () + { + int hash = 7; + hash = (41 * hash) + this.weight; + + return hash; + } + + //----------// + // toString // + //----------// + @Override + public String toString () + { + StringBuilder sb = new StringBuilder("{SSig"); + sb.append(" weight=") + .append(weight); + + if (bounds != null) { + sb.append(" Rectangle[x=") + .append(bounds.x) + .append(",y=") + .append(bounds.y) + .append(",width=") + .append(bounds.width) + .append(",height=") + .append(bounds.height) + .append("]"); + } + + sb.append("}"); + + return sb.toString(); + } +} diff --git a/src/main/omr/lag/Sections.java b/src/main/omr/lag/Sections.java new file mode 100644 index 0000000..8c553ed --- /dev/null +++ b/src/main/omr/lag/Sections.java @@ -0,0 +1,221 @@ +//----------------------------------------------------------------------------// +// // +// S e c t i o n s // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.lag; + +import omr.run.Orientation; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.awt.Rectangle; +import java.util.Arrays; +import java.util.Collection; +import java.util.Comparator; +import java.util.LinkedHashSet; +import java.util.Set; + +/** + * Class {@code Sections} handles features related to a collection of + * sections. + * + * @author Hervé Bitteur + */ +public class Sections +{ + //~ Static fields/initializers --------------------------------------------- + + /** Usual logger utility */ + private static final Logger logger = LoggerFactory.getLogger(Sections.class); + + //~ Methods ---------------------------------------------------------------- + //-----------// + // getBounds // + //-----------// + /** + * Return the display bounding box of a collection of sections. + * + * @param sections the provided collection of sections + * @return the bounding contour + */ + public static Rectangle getBounds (Collection sections) + { + Rectangle box = null; + + for (Section section : sections) { + if (box == null) { + box = new Rectangle(section.getBounds()); + } else { + box.add(section.getBounds()); + } + } + + return box; + } + + //----------------------------// + // getReverseLengthComparator // + //----------------------------// + /** + * Return a comparator for comparing Section instances on their + * decreasing length, using the provided orientation. + * + * @param orientation the provided orientation + */ + public static Comparator
getReverseLengthComparator ( + final Orientation orientation) + { + return new Comparator
() + { + @Override + public int compare (Section s1, + Section s2) + { + return Integer.signum( + s2.getLength(orientation) - s1.getLength(orientation)); + } + }; + } + + //---------------------------// + // lookupIntersectedSections // + //---------------------------// + /** + * Convenient method to look for sections that intersect the + * provided rectangle + * + * @param rect provided rectangle + * @param sections the collection of sections to browse + * @return the set of intersecting sections + */ + public static Set
lookupIntersectedSections (Rectangle rect, + Collection sections) + { + Set
found = new LinkedHashSet<>(); + + for (Section section : sections) { + if (section.intersects(rect)) { + found.add(section); + } + } + + return found; + } + + //----------------// + // lookupSections // + //----------------// + /** + * Convenient method to look for sections contained by the provided + * rectangle + * + * @param rect provided rectangle + * @param sections the collection of sections to browse + * @return the set of contained sections + */ + public static Set
lookupSections (Rectangle rect, + Collection sections) + { + Set
found = new LinkedHashSet<>(); + + for (Section section : sections) { + if (rect.contains(section.getBounds())) { + found.add(section); + } + } + + return found; + } + + //----------// + // toString // + //----------// + /** + * Convenient method, to build a string with just the ids of the + * section collection, introduced by the provided label. + * + * @param label the string that introduces the list of IDs + * @param sections the collection of sections + * @return the string built + */ + public static String toString (String label, + Collection sections) + { + if (sections == null) { + return ""; + } + + StringBuilder sb = new StringBuilder(); + sb.append(label) + .append("["); + + for (Section section : sections) { + sb.append("#") + .append(section.isVertical() ? "V" : "H") + .append(section.getId()); + } + + sb.append("]"); + + return sb.toString(); + } + + //----------// + // toString // + //----------// + /** + * Convenient method, to build a string with just the ids of the + * section array, introduced by the provided label. + * + * @param label the string that introduces the list of IDs + * @param sections the array of sections + * @return the string built + */ + public static String toString (String label, + Section... sections) + { + return toString(label, Arrays.asList(sections)); + } + + //----------// + // toString // + //----------// + /** + * Convenient method, to build a string with just the ids of the + * section collection, introduced by the label "sections". + * + * @param sections the collection of sections + * @return the string built + */ + public static String toString (Collection sections) + { + return toString("sections", sections); + } + + //----------// + // toString // + //----------// + /** + * Convenient method, to build a string with just the ids of the + * section array, introduced by the label "sections". + * + * @param sections the array of sections + * @return the string built + */ + public static String toString (Section... sections) + { + return toString("sections", sections); + } + + private Sections () + { + } +} diff --git a/src/main/omr/lag/SectionsBuilder.java b/src/main/omr/lag/SectionsBuilder.java new file mode 100644 index 0000000..792af15 --- /dev/null +++ b/src/main/omr/lag/SectionsBuilder.java @@ -0,0 +1,347 @@ +//----------------------------------------------------------------------------// +// // +// S e c t i o n s B u i l d e r // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.lag; + +import omr.run.PixelFilter; +import omr.run.Run; +import omr.run.RunsTable; +import omr.run.RunsTableFactory; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.List; + +/** + * Class {@code SectionsBuilder} populates a full lag, by building the + * lag sections and junctions, out of a provided {@link RunsTable} + * instance. + * + * @author Hervé Bitteur + */ +public class SectionsBuilder +{ + //~ Static fields/initializers --------------------------------------------- + + /** Usual logger utility */ + private static final Logger logger = LoggerFactory.getLogger( + SectionsBuilder.class); + + //~ Instance fields -------------------------------------------------------- + /** Policy for detection of junctions */ + private JunctionPolicy junctionPolicy; + + /** The lag to populate */ + private Lag lag; + + /** List of sections just created by createSections() */ + private List
created; + + /** All Active sections in the next column */ + private List
nextActives; + + /** + * List of sections in previous column that overlap given run in next column + */ + private List
overlappingSections; + + /** + * All Active sections in the previous column, i.e. only sections that have + * a run in previous column + */ + private List
prevActives; + + //~ Constructors ----------------------------------------------------------- + //-----------------// + // SectionsBuilder // + //-----------------// + /** + * Create an instance of SectionsBuilder. + * + * @param lag the lag to populate + * @param junctionPolicy the policy to detect junctions + */ + public SectionsBuilder (Lag lag, + JunctionPolicy junctionPolicy) + { + this.lag = lag; + this.junctionPolicy = junctionPolicy; + } + + //~ Methods ---------------------------------------------------------------- + //----------------// + // createSections // + //----------------// + /** + * Populate a lag by creating sections from the provided table of runs + * + * @param runsTable the table of runs + * @return the list of created sections + */ + public List
createSections (RunsTable runsTable) + { + // Get brand new collections + created = new ArrayList<>(); + nextActives = new ArrayList<>(); + overlappingSections = new ArrayList<>(); + prevActives = new ArrayList<>(); + + // All runs (if any) in first column start each their own section + for (Run run : runsTable.getSequence(0)) { + nextActives.add(createSection(0, run)); + } + + // Now scan each pair of columns, starting at 2nd column + for (int col = 1; col < runsTable.getSize(); col++) { + List runList = runsTable.getSequence(col); + + // If we have runs in this column + if (!runList.isEmpty()) { + // Copy the former next actives sections + // as the new previous active sections + prevActives = nextActives; + nextActives = new ArrayList<>(); + + // Process all sections of previous column, then prevActives + // will contain only active sections (that may be continued) + logger.debug("Prev column"); + + for (Section section : prevActives) { + processPrevSide(section, runList); + } + + // Process all runs of next column + logger.debug("Next column"); + + for (Run run : runList) { + processNextSide(col, run); + } + } else { + nextActives.clear(); + } + } + + // Some housekeeping + prevActives = null; + nextActives = null; + overlappingSections = null; + + // Reset proper Ids + for (Section section : lag.getVertices()) { + int id = section.getId(); + + if (id < 0) { + section.setId(-id); + } + } + + // Store the content of runs table into the lag + lag.addRuns(runsTable); + + return created; + } + + //----------------// + // createSections // + //----------------// + /** + * Populate a lag by creating sections directly out of a pixel source + * + * @param name a name assigned to the runs table + * @param source the source to read pixels from + * @param minRunLength minimum length to consider a run + * @return the list of created sections + */ + public List
createSections (String name, + PixelFilter source, + int minRunLength) + { + // Define a proper table factory + RunsTableFactory factory = new RunsTableFactory( + lag.getOrientation(), + source, + minRunLength); + + // Create the runs table + RunsTable table = factory.createTable(name); + + // Now proceed to section extraction + return createSections(table); + } + + //-----------------// + // continueSection // + //-----------------// + private void continueSection (Section section, + Run run) + { + logger.debug("Continuing section {} with {}", section, run); + + section.append(run); + nextActives.add(section); + } + + //---------------// + // createSection // + //---------------// + private Section createSection (int firstPos, + Run firstRun) + { + Section section = lag.createSection(firstPos, firstRun); + created.add(section); + + return section; + } + + //--------// + // finish // + //--------// + private void finish (Section section) + { + section.setId(-section.getId()); + } + + //------------// + // isFinished // + //------------// + private boolean isFinished (Section section) + { + return section.getId() < 0; + } + + //-----------------// + // processNextSide // + //-----------------// + /** + * Process NextSide takes care of the second column, at the given run, + * checking among the prevActives Sections which overlap this run. + */ + private void processNextSide (int col, + Run run) + { + logger.debug("processNextSide for run {}", run); + + int nextStart = run.getStart(); + int nextStop = run.getStop(); + + // Check if overlap with a section run in previous column + // All such sections are then stored in overlappingSections + overlappingSections.clear(); + + for (Section section : prevActives) { + Run lastRun = section.getLastRun(); + + if (lastRun.getStart() > nextStop) { + break; + } + + if (lastRun.getStop() >= nextStart) { + logger.debug("Overlap from {} to {}", lastRun, run); + overlappingSections.add(section); + } + } + + // Processing now depends on nb of overlapping runs + logger.debug("overlap={}", overlappingSections.size()); + + switch (overlappingSections.size()) { + case 0: // Begin a brand new section + nextActives.add(createSection(col, run)); + + break; + + case 1: // Continuing sections (if not finished) + + Section prevSection = overlappingSections.get(0); + + if (!isFinished(prevSection)) { + continueSection(prevSection, run); + } else { + // Create a new section, linked by a junction + Section sct = createSection(col, run); + nextActives.add(sct); + prevSection.addTarget(sct); + } + + break; + + default: // Converging sections, end them, start a new one + + logger.debug("Converging at {}", run); + Section newSection = createSection(col, run); + nextActives.add(newSection); + + for (Section section : overlappingSections) { + section.addTarget(newSection); + } + } + } + + //-----------------// + // processPrevSide // + //-----------------// + /** + * Take care of the first column, at the given section/run, + * checking links to the nextColumnRuns that overlap this run. + * + * @param section the section at hand + * @param nextColumnRuns runs of the next column + */ + private void processPrevSide (Section section, + List nextColumnRuns) + { + Run lastRun = section.getLastRun(); + int prevStart = lastRun.getStart(); + int prevStop = lastRun.getStop(); + logger.debug("processPrevSide for section {}", section); + + // Check if overlap with a run in next column + int overlapNb = 0; + Run overlapRun = null; + + for (Run run : nextColumnRuns) { + if (run.getStart() > prevStop) { + break; + } + + if (run.getStop() >= prevStart) { + logger.debug("Overlap from {} to {}", lastRun, run); + overlapNb++; + overlapRun = run; + } + } + + // Now consider how many overlapping runs we have in next column + logger.debug("overlap={}", overlapNb); + + switch (overlapNb) { + case 0: // Nothing : end of the section + logger.debug("Ending section {}", section); + break; + + case 1: // Continue if consistent + if (junctionPolicy.consistentRun(overlapRun, section)) { + logger.debug("Perhaps extending section {} with run {}", + section, overlapRun); + } else { + logger.debug("Incompatible height between {} and run {}", + section, overlapRun); + finish(section); + } + break; + + default: // Diverging, so conclude the section here + finish(section); + } + } +} diff --git a/src/main/omr/lag/package.html b/src/main/omr/lag/package.html new file mode 100644 index 0000000..ca5a391 --- /dev/null +++ b/src/main/omr/lag/package.html @@ -0,0 +1,19 @@ + + + + + + Package omr.lag + + + +

+ The lag package deals with LAGs (Linear Adjacency Graphs), a + specialized directed graph where vertices are sections linked by + junctions, a Section being a simply a connex collection of pixel + runs, and a Run a vector of foreground pixels. +

+ + + diff --git a/src/main/omr/lag/ui/SectionBoard.java b/src/main/omr/lag/ui/SectionBoard.java new file mode 100644 index 0000000..ea5147c --- /dev/null +++ b/src/main/omr/lag/ui/SectionBoard.java @@ -0,0 +1,444 @@ +//----------------------------------------------------------------------------// +// // +// S e c t i o n B o a r d // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.lag.ui; + +import omr.constant.Constant; +import omr.constant.ConstantSet; + +import omr.lag.Lag; +import omr.lag.Section; + +import omr.run.Orientation; + +import omr.selection.MouseMovement; +import omr.selection.SectionEvent; +import omr.selection.SectionIdEvent; +import omr.selection.SectionSetEvent; +import omr.selection.SelectionHint; +import omr.selection.UserEvent; + +import omr.stick.StickRelation; + +import omr.ui.Board; +import omr.ui.field.LIntegerField; +import omr.ui.field.SpinnerUtil; +import omr.ui.util.Panel; + +import com.jgoodies.forms.builder.PanelBuilder; +import com.jgoodies.forms.layout.CellConstraints; +import com.jgoodies.forms.layout.FormLayout; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.awt.Rectangle; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.util.Set; + +import javax.swing.JButton; +import javax.swing.JLabel; +import javax.swing.JSpinner; +import javax.swing.JTextField; +import javax.swing.event.ChangeEvent; +import javax.swing.event.ChangeListener; + +/** + * Class {@code SectionBoard} defines a board dedicated to the display + * of {@link omr.lag.Section} information, it can also be used as an + * input means by directly entering the section id in the proper Id + * spinner. + * + * @author Hervé Bitteur + */ +public class SectionBoard + extends Board +{ + //~ Static fields/initializers --------------------------------------------- + + /** Specific application parameters */ + private static final Constants constants = new Constants(); + + /** Usual logger utility */ + private static final Logger logger = LoggerFactory.getLogger( + SectionBoard.class); + + /** Events this board is interested in */ + private static final Class[] eventsRead = new Class[]{ + SectionEvent.class, + SectionSetEvent.class + }; + + //~ Instance fields -------------------------------------------------------- + /** Underlying lag */ + protected final Lag lag; + + /** Counter of section selection */ + protected final JLabel count = new JLabel(""); + + // Section input devices + // + /** Button for section dump */ + private final JButton dump; + + /** Spinner for section id */ + private final JSpinner id = new JSpinner(); + + // Output for plain Section + // + /** Field for left abscissa */ + private final LIntegerField x = new LIntegerField( + false, + "X", + "Left abscissa in pixels"); + + /** Field for top ordinate */ + private final LIntegerField y = new LIntegerField( + false, + "Y", + "Top ordinate in pixels"); + + /** Field for width */ + private final LIntegerField width = new LIntegerField( + false, + "Width", + "Horizontal width in pixels"); + + /** Field for height */ + private final LIntegerField height = new LIntegerField( + false, + "Height", + "Vertical height in pixels"); + + /** Field for weight */ + private final LIntegerField weight = new LIntegerField( + false, + "Weight", + "Number of pixels in this section"); + + // Additional output for StickSection (TODO: remove these fields) + // + /** Field for role in stick building */ + private final JTextField role = new JTextField(); + + private final LIntegerField direction = new LIntegerField( + false, + "Dir", + "Direction from the stick core"); + + /** Field for layer number */ + private final LIntegerField layer = new LIntegerField( + false, + "Layer", + "Layer number for this stick section"); + + /** To avoid loop, indicate that selecting is being done by the spinner */ + private boolean idSelecting = false; + + /** To avoid loop, indicate that update() method id being processed */ + private boolean updating = false; + + //~ Constructors ----------------------------------------------------------- + //--------------// + // SectionBoard // + //--------------// + /** + * Create a Section Board + * + * @param lag the related lag + * @param expanded true for initially expanded, false for collapsed + */ + public SectionBoard (final Lag lag, + boolean expanded) + { + super( + Board.SECTION.name + + ((lag.getOrientation() == Orientation.VERTICAL) ? " Vert" : " Hori"), + Board.SECTION.position + + ((lag.getOrientation() == Orientation.VERTICAL) ? 100 : 0), + lag.getSectionService(), + eventsRead, + true, // Dump + expanded); + + this.lag = lag; + + // Dump button + dump = getDumpButton(); + dump.setToolTipText("Dump this section"); + dump.addActionListener( + new ActionListener() + { + @Override + public void actionPerformed (ActionEvent e) + { + // Retrieve current section selection + Section section = (Section) lag.getSectionService() + .getSelection( + SectionEvent.class); + + if (section != null) { + section.dump(); + } + } + }); + dump.setEnabled(false); // Until a section selection is made + + // ID Spinner + id.setToolTipText("General spinner for any section id"); + id.addChangeListener( + new ChangeListener() + { + @Override + public void stateChanged (ChangeEvent e) + { + // Make sure this new Id value is due to user + // action on an Id spinner, and not the mere update + // of section fields (which include this id). + if (!updating) { + Integer sectionId = (Integer) id.getValue(); + logger.debug("sectionId={} for {}", sectionId, lag); + + idSelecting = true; + lag.getSectionService() + .publish( + new SectionIdEvent( + SectionBoard.this, + SelectionHint.SECTION_INIT, + sectionId)); + idSelecting = false; + } + } + }); + id.setModel(new SpinnerSectionModel(lag)); + SpinnerUtil.setEditable(id, true); + SpinnerUtil.setRightAlignment(id); + + // Relation + if (constants.hideRelationFields.getValue()) { + direction.setVisible(false); + layer.setVisible(false); + role.setVisible(false); + } + + role.setEditable(false); + role.setHorizontalAlignment(JTextField.CENTER); + role.setToolTipText("Role in the composition of the containing stick"); + + // Component layout + defineLayout(); + } + + //~ Methods ---------------------------------------------------------------- + //---------// + // onEvent // + //---------// + /** + * Call-back triggered when Section Selection has been modified + * + * @param event the section event + */ + @SuppressWarnings("unchecked") + @Override + public void onEvent (UserEvent event) + { + try { + // Ignore RELEASING + if (event.movement == MouseMovement.RELEASING) { + return; + } + + logger.debug("SectionBoard: {}", event); + + if (event instanceof SectionEvent) { + handleEvent((SectionEvent) event); + } else if (event instanceof SectionSetEvent) { + handleEvent((SectionSetEvent) event); + } + } catch (Exception ex) { + logger.warn(getClass().getName() + " onEvent error", ex); + } + } + + //--------------// + // defineLayout // + //--------------// + private void defineLayout () + { + FormLayout layout = Panel.makeFormLayout(4, 3); + PanelBuilder builder = new PanelBuilder(layout, getBody()); + builder.setDefaultDialogBorder(); + + CellConstraints cst = new CellConstraints(); + int r = 1; // -------------------------------- + + builder.add(count, cst.xy(9, r)); + + r += 2; // -------------------------------- + builder.addLabel("Id", cst.xy(1, r)); + builder.add(id, cst.xy(3, r)); + + builder.add(x.getLabel(), cst.xy(5, r)); + builder.add(x.getField(), cst.xy(7, r)); + + builder.add(width.getLabel(), cst.xy(9, r)); + builder.add(width.getField(), cst.xy(11, r)); + + r += 2; // -------------------------------- + builder.add(weight.getLabel(), cst.xy(1, r)); + builder.add(weight.getField(), cst.xy(3, r)); + + builder.add(y.getLabel(), cst.xy(5, r)); + builder.add(y.getField(), cst.xy(7, r)); + + builder.add(height.getLabel(), cst.xy(9, r)); + builder.add(height.getField(), cst.xy(11, r)); + + r += 2; // -------------------------------- + builder.add(layer.getLabel(), cst.xy(1, r)); + builder.add(layer.getField(), cst.xy(3, r)); + + builder.add(direction.getLabel(), cst.xy(5, r)); + builder.add(direction.getField(), cst.xy(7, r)); + + builder.add(role, cst.xyw(9, r, 3)); + } + + //-------------// + // handleEvent // + //-------------// + /** + * Interest in Section + * + * @param sectionEvent + */ + private void handleEvent (SectionEvent sectionEvent) + { + if (updating) { + return; + } + + try { + // Update section fields in this board + updating = true; + + final Section section = (sectionEvent != null) + ? sectionEvent.getData() : null; + dump.setEnabled(section != null); + + Integer sectionId = null; + + if (idSelecting) { + sectionId = (Integer) id.getValue(); + } + + emptyFields(getBody()); + + if (section == null) { + // If the user is currently using the Id spinner, make sure we + // display the right Id value in the spinner, even if there is + // no corresponding section + if (idSelecting) { + id.setValue(sectionId); + } else { + id.setValue(SpinnerUtil.NO_VALUE); + } + + if (constants.hideRelationFields.getValue()) { + direction.setVisible(false); + layer.setVisible(false); + role.setVisible(false); + } + } else { + // We have a valid section, let's display its fields + id.setValue(section.getId()); + + Rectangle box = section.getBounds(); + x.setValue(box.x); + y.setValue(box.y); + width.setValue(box.width); + height.setValue(box.height); + weight.setValue(section.getWeight()); + + // Additional relation fields for a StickSection + StickRelation relation = section.getRelation(); + + if (relation != null) { + if (constants.hideRelationFields.getValue()) { + layer.setVisible(true); + } + + layer.setValue(relation.layer); + + if (constants.hideRelationFields.getValue()) { + direction.setVisible(true); + } + + direction.setValue(relation.direction); + + if (relation.role != null) { + role.setText(relation.role.toString()); + + if (constants.hideRelationFields.getValue()) { + role.setVisible(true); + } + } else { + if (constants.hideRelationFields.getValue()) { + role.setVisible(false); + } + } + } else if (constants.hideRelationFields.getValue()) { + direction.setVisible(false); + layer.setVisible(false); + role.setVisible(false); + } + } + } finally { + updating = false; + } + } + + //-------------// + // handleEvent // + //-------------// + /** + * Interest in SectionSet + * + * @param sectionSetEvent + */ + private void handleEvent (SectionSetEvent sectionSetEvent) + { + // Display count of sections in the section set + Set
sections = sectionSetEvent.getData(); + + if ((sections != null) && !sections.isEmpty()) { + count.setText(Integer.toString(sections.size())); + } else { + count.setText(""); + } + } + + //~ Inner Classes ---------------------------------------------------------- + //-----------// + // Constants // + //-----------// + private static final class Constants + extends ConstantSet + { + //~ Instance fields ---------------------------------------------------- + + Constant.Boolean hideRelationFields = new Constant.Boolean( + true, + "Should we hide section relation fields when empty?"); + + } +} diff --git a/src/main/omr/lag/ui/SectionView.java b/src/main/omr/lag/ui/SectionView.java new file mode 100644 index 0000000..fb426fd --- /dev/null +++ b/src/main/omr/lag/ui/SectionView.java @@ -0,0 +1,74 @@ +//----------------------------------------------------------------------------// +// // +// S e c t i o n V i e w // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.lag.ui; + +import omr.graph.VertexView; + +import java.awt.Color; +import java.awt.Graphics; + +/** + * Class {@code SectionView} defines one view meant for display of a + * given section. + * + * @author Hervé Bitteur + */ +public interface SectionView + extends VertexView +{ + //~ Methods ---------------------------------------------------------------- + + /** + * Report the default color. This is the permanent default, which is used + * when the color is reset by {@link #resetColor} + * + * @return the section default color + */ + public Color getDefaultColor (); + + /** + * Report whether a default color has been assigned + * + * @return true if defaultColor is no longer null + */ + public boolean isColorized (); + + /** + * Render the section using the provided graphics object, while + * showing that the section has been selected. + * + * @param g the graphics environment (which may be applying transformation + * such as scale) + * @return true if the section is concerned by the clipping rectangle, which + * means if (part of) the section has been drawn + */ + public boolean renderSelected (Graphics g); + + /** + * Allow to reset to default the display color of a given section + */ + public void resetColor (); + + /** + * Allow to modify the display color of a given section. + * + * @param color the new color + */ + public void setColor (Color color); + + /** + * Set the default color. + * + * @param color the default color for this section + */ + public void setDefaultColor (Color color); +} diff --git a/src/main/omr/lag/ui/SpinnerSectionModel.java b/src/main/omr/lag/ui/SpinnerSectionModel.java new file mode 100644 index 0000000..cac9b72 --- /dev/null +++ b/src/main/omr/lag/ui/SpinnerSectionModel.java @@ -0,0 +1,156 @@ +//----------------------------------------------------------------------------// +// // +// S p i n n e r S e c t i o n M o d e l // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.lag.ui; + +import omr.lag.Lag; + +import omr.ui.field.SpinnerUtil; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.swing.AbstractSpinnerModel; + +/** + * Class {@code SpinnerSectionModel} is a spinner model backed by a + * {@link Lag}. + * Any modification in the lag is thus transparently handled, + * since the lag is the model. + * + * @author Hervé Bitteur + */ +public class SpinnerSectionModel + extends AbstractSpinnerModel +{ + //~ Static fields/initializers --------------------------------------------- + + /** Usual logger utility */ + private static final Logger logger = LoggerFactory.getLogger( + SpinnerSectionModel.class); + + //~ Instance fields -------------------------------------------------------- + /** Underlying section lag */ + private final Lag lag; + + /** Current section id */ + private Integer currentId; + + //~ Constructors ----------------------------------------------------------- + //---------------------// + // SpinnerSectionModel // + //---------------------// + /** + * Creates a new SpinnerSectionModel object, on all lag sections + * + * @param lag the underlying section lag + */ + public SpinnerSectionModel (Lag lag) + { + if (lag == null) { + throw new IllegalArgumentException( + "SpinnerSectionModel expects non-null section lag"); + } + + this.lag = lag; + + currentId = SpinnerUtil.NO_VALUE; + } + + //~ Methods ---------------------------------------------------------------- + //--------------// + // getNextValue // + //--------------// + /** + * Return the next legal section id in the sequence that comes after + * the section id returned by {@code getValue()}. + * If the end of the sequence has been reached then return null. + * + * @return the next legal section id or null if one doesn't exist + */ + @Override + public Object getNextValue () + { + final int cur = currentId.intValue(); + logger.debug("getNextValue cur={}", cur); + + if (cur == SpinnerUtil.NO_VALUE) { + return (lag.getLastVertexId() > 0) ? 1 : null; + } else { + return (cur < lag.getLastVertexId()) ? (cur + 1) : null; + } + } + + //------------------// + // getPreviousValue // + //------------------// + /** + * Return the legal section id in the sequence that comes before the + * section id returned by {@code getValue()}. + * If the end of the sequence has been reached then return null. + * + * @return the previous legal value or null if one doesn't exist + */ + @Override + public Object getPreviousValue () + { + final int cur = currentId.intValue(); + logger.debug("getPreviousValue cur={}", cur); + + if (cur == SpinnerUtil.NO_VALUE) { + return null; + } else { + return (cur > 1) ? (cur - 1) : null; + } + } + + //----------// + // getValue // + //----------// + /** + * The current element of the sequence. + * + * @return the current spinner value. + */ + @Override + public Object getValue () + { + logger.debug("getValue currentId={}", currentId); + + return currentId; + } + + //----------// + // setValue // + //----------// + /** + * Changes current section id of the model. + * If the section id is illegal then an {@code IllegalArgumentException} is + * thrown. + * + * @param value the value to set + * @exception IllegalArgumentException if {@code value} isn't allowed + */ + @Override + public void setValue (Object value) + { + logger.debug("setValue value={}", value); + + Integer id = (Integer) value; + + if ((id >= 0) && (id <= lag.getLastVertexId())) { + currentId = id; + fireStateChanged(); + } else { + logger.warn("Invalid section id: {}", id); + } + } +} diff --git a/src/main/omr/lag/ui/package.html b/src/main/omr/lag/ui/package.html new file mode 100644 index 0000000..c577051 --- /dev/null +++ b/src/main/omr/lag/ui/package.html @@ -0,0 +1,18 @@ + + + + + + Package omr.lag.ui + + + + +

+ Package dedicated to the handling of lag related UI +

+ + + diff --git a/src/main/omr/log/LogGuiAppender.java b/src/main/omr/log/LogGuiAppender.java new file mode 100644 index 0000000..8dffd60 --- /dev/null +++ b/src/main/omr/log/LogGuiAppender.java @@ -0,0 +1,77 @@ +//----------------------------------------------------------------------------// +// // +// L o g G u i A p p e n d e r // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.log; + +import omr.Main; + +import omr.ui.MainGui; + +import ch.qos.logback.classic.spi.ILoggingEvent; +import ch.qos.logback.core.AppenderBase; + +import java.util.concurrent.ArrayBlockingQueue; + +/** + * Class {@code LogGuiAppender} is a log appender that appends the + * logging messages to the GUI. + * It uses an intermediate queue to cope with initial conditions when the GUI + * is not yet ready to accept messages. + * + * @author Hervé Bitteur + */ +public class LogGuiAppender + extends AppenderBase +{ + //~ Static fields/initializers --------------------------------------------- + + /** + * Size of the mail box. + * (This cannot be an application Constant, for elaboration dependencies) + */ + private static final int LOG_MBX_SIZE = 10000; + + /** Temporary mail box for logged messages. */ + private static ArrayBlockingQueue logMbx = new ArrayBlockingQueue( + LOG_MBX_SIZE); + + //~ Methods ---------------------------------------------------------------- + //---------------// + // getEventCount // + //---------------// + public static int getEventCount () + { + return logMbx.size(); + } + + //-----------// + // pollEvent // + //-----------// + public static ILoggingEvent pollEvent () + { + return logMbx.poll(); + } + + //--------// + // append // + //--------// + @Override + protected void append (ILoggingEvent event) + { + logMbx.offer(event); + + MainGui gui = Main.getGui(); + + if (gui != null) { + gui.notifyLog(); + } + } +} diff --git a/src/main/omr/log/LogPane.java b/src/main/omr/log/LogPane.java new file mode 100644 index 0000000..d11266d --- /dev/null +++ b/src/main/omr/log/LogPane.java @@ -0,0 +1,194 @@ +//----------------------------------------------------------------------------// +// // +// L o g P a n e // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.log; + +import omr.constant.Constant; +import omr.constant.ConstantSet; + +import ch.qos.logback.classic.Level; +import ch.qos.logback.classic.spi.ILoggingEvent; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.awt.Color; +import java.awt.Insets; + +import javax.swing.JComponent; +import javax.swing.JScrollPane; +import javax.swing.JTextPane; +import javax.swing.SwingUtilities; +import javax.swing.text.AbstractDocument; +import javax.swing.text.BadLocationException; +import javax.swing.text.SimpleAttributeSet; +import javax.swing.text.StyleConstants; + +/** + * Class {@code LogPane} defines the pane dedicated to application-level + * messages, those that are logged using the {@code Logger} class. + * + * @author Hervé Bitteur + */ +public class LogPane +{ + //~ Static fields/initializers --------------------------------------------- + + /** Specific application parameters */ + private static final Constants constants = new Constants(); + + /** Usual logger utility */ + private static final Logger logger = LoggerFactory.getLogger(LogPane.class); + + //~ Instance fields -------------------------------------------------------- + /** The scrolling text area */ + private JScrollPane component; + + /** Status/log area */ + private final JTextPane logArea; + + private final AbstractDocument document; + + private final SimpleAttributeSet attributes = new SimpleAttributeSet(); + + //~ Constructors ----------------------------------------------------------- + //---------// + // LogPane // + //---------// + /** + * Create the log pane, with a standard mailbox. + */ + public LogPane () + { + // Build the scroll pane + component = new JScrollPane(); + component.setBorder(null); + + // log/status area + logArea = new JTextPane(); + logArea.setEditable(false); + logArea.setMargin(new Insets(5, 5, 5, 5)); + document = (AbstractDocument) logArea.getStyledDocument(); + + // Let the scroll pane display the log area + component.setViewportView(logArea); + } + + //~ Methods ---------------------------------------------------------------- + //----------// + // clearLog // + //----------// + /** + * Clear the current content of the log + */ + public void clearLog () + { + logArea.setText(""); + logArea.setCaretPosition(0); + component.repaint(); + } + + //--------------// + // getComponent // + //--------------// + /** + * Give access to the real component + * + * @return the concrete component + */ + public JComponent getComponent () + { + return component; + } + + //-----------// + // notifyLog // + //-----------// + /** + * Notify that there is one or more log records in the Logger mailbox. + */ + public void notifyLog () + { + SwingUtilities.invokeLater( + new Runnable() + { + @Override + public void run () + { + while (LogGuiAppender.getEventCount() > 0) { + ILoggingEvent event = LogGuiAppender.pollEvent(); + + if (event != null) { + // Color + StyleConstants.setForeground( + attributes, + getLevelColor(event.getLevel())); + + // Font name + StyleConstants.setFontFamily( + attributes, + constants.fontName.getValue()); + + // Font size + StyleConstants.setFontSize( + attributes, + constants.fontSize.getValue()); + + try { + document.insertString( + document.getLength(), + event.getFormattedMessage() + "\n", + attributes); + } catch (BadLocationException ex) { + ex.printStackTrace(); + } + } + } + } + }); + } + + //---------------// + // getLevelColor // + //---------------// + private Color getLevelColor (Level level) + { + if (level.isGreaterOrEqual(Level.ERROR)) { + return Color.RED; + } else if (level.isGreaterOrEqual(Level.WARN)) { + return Color.BLUE; + } else if (level.isGreaterOrEqual(Level.INFO)) { + return Color.BLACK; + } else { + return Color.GRAY; + } + } + + //~ Inner Classes ---------------------------------------------------------- + //-----------// + // Constants // + //-----------// + private static final class Constants + extends ConstantSet + { + //~ Instance fields ---------------------------------------------------- + + Constant.Integer fontSize = new Constant.Integer( + "Points", + 10, + "Font size for log pane"); + + Constant.String fontName = new Constant.String( + "Lucida Console", + "Font name for log pane"); + + } +} diff --git a/src/main/omr/log/LogStepAppender.java b/src/main/omr/log/LogStepAppender.java new file mode 100644 index 0000000..aa8599a --- /dev/null +++ b/src/main/omr/log/LogStepAppender.java @@ -0,0 +1,41 @@ +//----------------------------------------------------------------------------// +// // +// L o g S t e p A p p e n d e r // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.log; + +import omr.step.Stepping; + +import ch.qos.logback.classic.Level; +import ch.qos.logback.classic.spi.ILoggingEvent; +import ch.qos.logback.core.AppenderBase; + +/** + * Class {@code LogStepAppender} uses the flow of logging messages + * (assumed to be filtered on INFO level at least) to notify a slight + * progress. + * Filtering on level is performed in the logging configuration file (if any). + * + * @author Hervé Bitteur + */ +public class LogStepAppender + extends AppenderBase +{ + //~ Methods ---------------------------------------------------------------- + + @Override + protected void append (ILoggingEvent event) + { + if (event.getLevel() + .toInt() >= Level.INFO_INT) { + Stepping.notifyProgress(); + } + } +} diff --git a/src/main/omr/log/LogUtil.java b/src/main/omr/log/LogUtil.java new file mode 100644 index 0000000..9b0f412 --- /dev/null +++ b/src/main/omr/log/LogUtil.java @@ -0,0 +1,188 @@ +//----------------------------------------------------------------------------// +// // +// L o g U t i l // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.log; + +import ch.qos.logback.classic.Level; +import ch.qos.logback.classic.Logger; +import ch.qos.logback.classic.LoggerContext; +import ch.qos.logback.classic.encoder.PatternLayoutEncoder; +import ch.qos.logback.core.Appender; +import ch.qos.logback.core.ConsoleAppender; +import ch.qos.logback.core.FileAppender; +import ch.qos.logback.core.util.StatusPrinter; + +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.nio.file.Paths; +import java.text.SimpleDateFormat; +import java.util.Date; + +/** + * Class {@code LogUtil} handles logging features that depend on + * underlying LogBack binding. + * + * @author Hervé Bitteur + */ +public class LogUtil +{ + //~ Static fields/initializers --------------------------------------------- + + /** System property for LogBack configuration. */ + private static final String LOGBACK_LOGGING_KEY = "logback.configurationFile"; + + /** File name for LogBack configuration. */ + private static final String LOGBACK_FILE_NAME = "logback.xml"; + + //~ Methods ---------------------------------------------------------------- + //------------// + // initialize // + //------------// + /** + * Check for (BackLog) logging configuration, and if not found, + * define a minimal configuration. + * This method should be called at the very beginning of the program before + * any logging request is sent. + * + * @param CONFIG_FOLDER Config folder which may contain a logback.xml file + * @param TEMP_FOLDER Temporary folder where log file should be written + */ + public static void initialize (File CONFIG_FOLDER, + File TEMP_FOLDER) + { + // 1/ Check if system property is set and points to a real file + final String loggingProp = System.getProperty(LOGBACK_LOGGING_KEY); + + if (loggingProp != null) { + File configFile = new File(loggingProp); + + if (configFile.exists()) { + // Everything seems OK, let LogBack use the config file + System.out.println("Using " + configFile.getAbsolutePath()); + + return; + } else { + System.out.println( + "File " + configFile.getAbsolutePath() + + " does not exist."); + } + } else { + System.out.println( + "Property " + LOGBACK_LOGGING_KEY + " not defined."); + } + + // 2/ Look for well-known location + File configFile = new File(CONFIG_FOLDER, LOGBACK_FILE_NAME); + + if (configFile.exists()) { + System.out.println("Using " + configFile.getAbsolutePath()); + + // Set property for logback + System.setProperty(LOGBACK_LOGGING_KEY, configFile.toString()); + + return; + } else { + System.out.println("Could not find " + configFile); + } + + // 3/ We need a default configuration + LoggerContext loggerContext = (LoggerContext) LoggerFactory.getILoggerFactory(); + Logger root = (ch.qos.logback.classic.Logger) LoggerFactory.getLogger( + Logger.ROOT_LOGGER_NAME); + + // CONSOLE + ConsoleAppender consoleAppender = new ConsoleAppender(); + PatternLayoutEncoder consoleEncoder = new PatternLayoutEncoder(); + consoleAppender.setName("CONSOLE"); + consoleAppender.setContext(loggerContext); + consoleEncoder.setContext(loggerContext); + consoleEncoder.setPattern("%-5level %caller{1} - %msg%ex%n"); + consoleEncoder.start(); + consoleAppender.setEncoder(consoleEncoder); + consoleAppender.start(); + root.addAppender(consoleAppender); + + // FILE (located in default temp directory) + File logFile; + FileAppender fileAppender = new FileAppender(); + PatternLayoutEncoder fileEncoder = new PatternLayoutEncoder(); + fileAppender.setName("FILE"); + fileAppender.setContext(loggerContext); + fileAppender.setAppend(false); + + String now = new SimpleDateFormat("yyyyMMdd'T'HHmmss").format( + new Date()); + logFile = Paths.get( + System.getProperty("java.io.tmpdir"), + "audiveris-" + now + ".log") + .toFile(); + fileAppender.setFile(logFile.getAbsolutePath()); + fileEncoder.setContext(loggerContext); + fileEncoder.setPattern("%date %level \\(%file:%line\\) - %msg%ex%n"); + fileEncoder.start(); + fileAppender.setEncoder(fileEncoder); + fileAppender.start(); + root.addAppender(fileAppender); + + // GUI (filtered in LogGuiAppender) + Appender guiAppender = new LogGuiAppender(); + guiAppender.setName("GUI"); + guiAppender.setContext(loggerContext); + guiAppender.start(); + root.addAppender(guiAppender); + + // STEP + Appender stepAppender = new LogStepAppender(); + stepAppender.setName("STEP"); + stepAppender.setContext(loggerContext); + stepAppender.start(); + root.addAppender(stepAppender); + + // Levels + root.setLevel(Level.INFO); + + // OPTIONAL: print logback internal status messages + StatusPrinter.print(loggerContext); + + root.info("Logging to file {}", logFile.getAbsolutePath()); + } + + //---------// + // toLevel // + //---------// + public static Level toLevel (final String str) + { + switch (str.toUpperCase()) { + case "ALL": + return Level.ALL; + + case "TRACE": + return Level.TRACE; + + case "DEBUG": + return Level.DEBUG; + + case "INFO": + return Level.INFO; + + case "WARN": + return Level.WARN; + + case "ERROR": + return Level.ERROR; + + default: + case "OFF": + return Level.OFF; + } + } +} diff --git a/src/main/omr/log/LoggingStream.java b/src/main/omr/log/LoggingStream.java new file mode 100644 index 0000000..6a19eb4 --- /dev/null +++ b/src/main/omr/log/LoggingStream.java @@ -0,0 +1,79 @@ +//----------------------------------------------------------------------------// +// // +// L o g g i n g S t r e a m // +// // +//----------------------------------------------------------------------------// +package omr.log; + +import omr.WellKnowns; + +import ch.qos.logback.classic.Level; + +import org.slf4j.Logger; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; + +/** + * Class {@code LoggingStream} defines an OutputStream that writes + * contents to a Logger upon each call to flush(). + *
+ * See + * blog of Nick Stephen + * + * @author Nick Stephen + */ +public class LoggingStream + extends ByteArrayOutputStream +{ + //~ Instance fields -------------------------------------------------------- + + private final Logger logger; + + private final Level level; + + //~ Constructors ----------------------------------------------------------- + /** + * Constructor + * + * @param logger Logger to write to + * @param level Level at which to write the log message + */ + public LoggingStream (Logger logger, + Level level) + { + super(); + this.logger = logger; + this.level = level; + } + + //~ Methods ---------------------------------------------------------------- + /** + * Upon flush(), write the existing contents of the OutputStream to + * the logger as a log record. + * + * @throws java.io.IOException in case of error + */ + @Override + public void flush () + throws IOException + { + String record; + + synchronized (this) { + super.flush(); + record = this.toString(WellKnowns.FILE_ENCODING); + super.reset(); + } + + // Avoid empty records + if ((record.length() == 0) || record.equals(WellKnowns.LINE_SEPARATOR)) { + return; + } + + // Write to the actual logger + ch.qos.logback.classic.Logger theLogger = (ch.qos.logback.classic.Logger) logger; + theLogger.log(null, null, level.toInt(), record, null, null); + } +} diff --git a/src/main/omr/log/package.html b/src/main/omr/log/package.html new file mode 100644 index 0000000..071cb76 --- /dev/null +++ b/src/main/omr/log/package.html @@ -0,0 +1,16 @@ + + + + + + Package omr.log + + + +

+ Package dedicated to logging +

+ + + diff --git a/src/main/omr/math/Barycenter.java b/src/main/omr/math/Barycenter.java new file mode 100644 index 0000000..489c73c --- /dev/null +++ b/src/main/omr/math/Barycenter.java @@ -0,0 +1,180 @@ +//----------------------------------------------------------------------------// +// // +// B a r y c e n t e r // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.math; + +import java.awt.geom.Point2D; + +/** + * Class {@code Barycenter} is meant to cumulate data when computing + * barycenter. + */ +public class Barycenter +{ + //~ Instance fields -------------------------------------------------------- + + /** + * The total weight (such as the number of pixels). + * At any time, the barycenter coordinates are respectively xx/weight + * and yy/weight + */ + private double weight; + + /** The weighted abscissa */ + private double xx; + + /** The weighted ordinate */ + private double yy; + + //~ Constructors ----------------------------------------------------------- + //------------// + // Barycenter // + //------------// + /** + * Creates a new Barycenter object. + */ + public Barycenter () + { + } + + //~ Methods ---------------------------------------------------------------- + //-----------// + // getWeight // + //-----------// + public final double getWeight () + { + return weight; + } + + //------// + // getX // + //------// + /** + * Report the current barycenter abscissa. + * + * @return current abscissa + */ + public final double getX () + { + return xx / weight; + } + + //------// + // getY // + //------// + /** + * Report the current barycenter ordinate. + * + * @return current ordinate + */ + public final double getY () + { + return yy / weight; + } + + //---------// + // include // + //---------// + /** + * Include another barycenter. + * + * @param weight total weight of this other barycenter + * @param x abscissa + * @param y ordinate + */ + public final void include (double weight, + double x, + double y) + { + this.weight += weight; + this.xx += (x * weight); + this.yy += (y * weight); + } + + //---------// + // include // + //---------// + /** + * Include another barycenter. + * + * @param that the other barycenter to include + */ + public final void include (Barycenter that) + { + this.weight += that.weight; + this.xx += that.xx; + this.yy += that.yy; + } + + //---------// + // include // + //---------// + /** + * Include one point (with default weight assigned to 1). + * + * @param x point abscissa + * @param y point ordinate + */ + public final void include (double x, + double y) + { + include(1, x, y); + } + + //---------// + // include // + //---------// + /** + * Include one point (with default weight assigned to 1). + * + * @param point point to include + */ + public final void include (Point2D point) + { + include(1, point.getX(), point.getY()); + } + + //---------// + // include // + //---------// + /** + * Include one point. + * + * @param weight weight assigned to the point + * @param point point to include + */ + public final void include (double weight, + Point2D point) + { + include(weight, point.getX(), point.getY()); + } + + //----------// + // toString // + //----------// + @Override + public String toString () + { + StringBuilder sb = new StringBuilder("{"); + sb.append(getClass().getSimpleName()) + .append(" weight:") + .append(weight); + + if (weight > 0) { + sb.append(" x:") + .append((float) getX()) + .append(" y:") + .append((float) getY()); + } + + return sb.toString(); + } +} diff --git a/src/main/omr/math/BasicLine.java b/src/main/omr/math/BasicLine.java new file mode 100644 index 0000000..4492757 --- /dev/null +++ b/src/main/omr/math/BasicLine.java @@ -0,0 +1,518 @@ +//----------------------------------------------------------------------------// +// // +// B a s i c L i n e // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.math; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import static java.lang.Math.*; + +/** + * Class {@code BasicLine} is a basic Line implementation which switches + * between horizontal and vertical equations when computing the points + * regression + * + * @author Hervé Bitteur + */ +public class BasicLine + implements Line +{ + //~ Static fields/initializers --------------------------------------------- + + /** Usual logger utility */ + private static final Logger logger = LoggerFactory.getLogger( + BasicLine.class); + + //~ Instance fields -------------------------------------------------------- + /** Flag to indicate that data needs to be recomputed */ + private boolean dirty; + + /** Orientation indication */ + private boolean isRatherVertical = false; + + /** x coeff in Line equation */ + private double a; + + /** y coeff in Line equation */ + private double b; + + /** 1 coeff in Line equation */ + private double c; + + /** Sigma (x) */ + private double sx; + + /** Sigma (x**2) */ + private double sx2; + + /** Sigma (x*y) */ + private double sxy; + + /** Sigma (y) */ + private double sy; + + /** Sigma (y**2) */ + private double sy2; + + /** For regression : Number of points */ + private int n; + + //~ Constructors ----------------------------------------------------------- + //-----------// + // BasicLine // + //-----------// + /** + * Creates a line, with no data. The line is no yet usable, except for + * including further defining points. + */ + public BasicLine () + { + reset(); + } + + //-----------// + // BasicLine // + //-----------// + /** + * Creates a line, for which we already know the coefficients. The + * coefficients don't have to be normalized, the constructor takes care of + * this. This line is not meant to be modified by including additional + * points (although this is doable), since it contains no defining points. + * + * @param a xCoeff + * @param b yCoeff + * @param c 1Coeff + */ + public BasicLine (double a, + double b, + double c) + { + this(); + + this.a = a; + this.b = b; + this.c = c; + + normalize(); + dirty = false; + + checkLineParameters(); + } + + //-----------// + // BasicLine // + //-----------// + /** + * Creates a line (and immediately compute its coefficients), as the least + * square fitted line on the provided points population. + * + * @param xVals abscissas of the points + * @param yVals ordinates of the points + */ + public BasicLine (double[] xVals, + double[] yVals) + { + this(); + + // Checks for parameter validity + if ((xVals == null) || (yVals == null)) { + throw new IllegalArgumentException( + "Provided arrays may not be null"); + } + + // Checks for parameter validity + if (xVals.length != yVals.length) { + throw new IllegalArgumentException( + "Provided arrays have different lengths"); + } + + // Checks for parameter validity + if (xVals.length < 2) { + throw new IllegalArgumentException("Provided arrays are too short"); + } + + // Include all defining points + for (int i = xVals.length - 1; i >= 0; i--) { + includePoint(xVals[i], yVals[i]); + } + + checkLineParameters(); + } + + //~ Methods ---------------------------------------------------------------- + //------------// + // distanceOf // + //------------// + @Override + public double distanceOf (double x, + double y) + { + checkLineParameters(); + + return (a * x) + (b * y) + c; + } + + //------------------// + // getInvertedSlope // + //------------------// + @Override + public double getInvertedSlope () + { + checkLineParameters(); + + return -b / a; + } + + //-----------------// + // getMeanDistance // + //-----------------// + @Override + public double getMeanDistance () + { + // Check we have at least 2 points + if (n < 2) { + throw new UndefinedLineException( + "Not enough defining points : " + n); + } + + checkLineParameters(); + + // abs is used in case of rounding errors + return sqrt( + abs( + (a * a * sx2) + (b * b * sy2) + (c * c * n) + + (2 * a * b * sxy) + (2 * a * c * sx) + (2 * b * c * sy)) / n); + } + + //-------------------// + // getNumberOfPoints // + //-------------------// + @Override + public int getNumberOfPoints () + { + return n; + } + + //----------// + // getSlope // + //----------// + @Override + public double getSlope () + { + checkLineParameters(); + + return -a / b; + } + + //-------------// + // includeLine // + //-------------// + @Override + public Line includeLine (Line other) + { + if (other instanceof BasicLine) { + BasicLine o = (BasicLine) other; + n += o.n; + sx += o.sx; + sy += o.sy; + sx2 += o.sx2; + sy2 += o.sy2; + sxy += o.sxy; + } else { + throw new RuntimeException("Combining inconsistent lines"); + } + + dirty = true; + + return this; + } + + //--------------// + // includePoint // + //--------------// + @Override + public void includePoint (double x, + double y) + { + logger.debug("includePoint x={} y={}", x, y); + + n += 1; + sx += x; + sy += y; + sx2 += (x * x); + sy2 += (y * y); + sxy += (x * y); + + dirty = true; + } + + //--------------// + // isHorizontal // + //--------------// + @Override + public boolean isHorizontal () + { + checkLineParameters(); + + return a == 0d; + } + + //------------// + // isVertical // + //------------// + @Override + public boolean isVertical () + { + checkLineParameters(); + + return b == 0d; + } + + //-------// + // reset // + //-------// + @Override + public void reset () + { + a = b = c = Double.NaN; + n = 0; + sx = sy = sx2 = sy2 = sxy = 0d; + + dirty = false; + } + + //--------------------// + // swappedCoordinates // + //--------------------// + /** + * Return a new line whose coordinates are swapped with respect to this one + * + * @return a new X/Y swapped line + */ + @Override + public Line swappedCoordinates () + { + BasicLine that = new BasicLine(); + + that.n = n; + that.sx = sy; + that.sy = sx; + that.sx2 = sy2; + that.sy2 = sx2; + that.sxy = sxy; + + that.dirty = true; + + return that; + } + + //----------// + // toString // + //----------// + @Override + public String toString () + { + try { + if (dirty) { + compute(); + } + + StringBuilder sb = new StringBuilder(); + + if (isRatherVertical) { + sb.append("{VLine "); + } else { + sb.append("{HLine "); + } + + if (a >= 0) { + sb.append(" "); + } + + sb.append((float) a) + .append("*x "); + + if (b >= 0) { + sb.append("+"); + } + + sb.append((float) b) + .append("*y "); + + if (c >= 0) { + sb.append("+"); + } + + sb.append((float) c) + .append("}"); + + return sb.toString(); + } catch (UndefinedLineException ex) { + return "INVALID LINE"; + } + } + + //------// + // xAtY // + //------// + @Override + public double xAtY (double y) + { + if (n == 1) { + return sx; + } + + checkLineParameters(); + + if (a != 0d) { + return -((b * y) + c) / a; + } else { + throw new NonInvertibleLineException("Line is horizontal"); + } + } + + //------// + // xAtY // + //------// + @Override + public int xAtY (int y) + { + return (int) rint(xAtY((double) y)); + } + + //------// + // yAtX // + //------// + @Override + public double yAtX (double x) + { + if (n == 1) { + return sy; + } + + checkLineParameters(); + + if (b != 0d) { + return ((-a * x) - c) / b; + } else { + throw new NonInvertibleLineException("Line is vertical"); + } + } + + //------// + // yAtX // + //------// + @Override + public int yAtX (int x) + { + return (int) rint(yAtX((double) x)); + } + + //------// + // getA // Meant for test + //------// + double getA () + { + checkLineParameters(); + + return a; + } + + //------// + // getB // Meant for test + //------// + double getB () + { + checkLineParameters(); + + return b; + } + + //------// + // getC // Meant for test + //------// + double getC () + { + checkLineParameters(); + + return c; + } + + //---------------------// + // checkLineParameters // + //---------------------// + /** + * Make sure the line parameters are usable. + */ + private void checkLineParameters () + { + // Recompute parameters based on points if so needed + if (dirty) { + compute(); + } + + // Make sure the parameters are available + if (Double.isNaN(a) || Double.isNaN(b) || Double.isNaN(c)) { + throw new UndefinedLineException( + "Line parameters not properly set"); + } + } + + //---------// + // compute // + //---------// + /** + * Compute the line equation, based on the cumulated number of points + */ + private void compute () + { + if (n < 2) { + throw new UndefinedLineException( + "Not enough defining points : " + n); + } + + // Make a choice between horizontal vs vertical + double hDen = (n * sx2) - (sx * sx); + double vDen = (n * sy2) - (sy * sy); + logger.debug("hDen={} vDen={}", hDen, vDen); + + if (abs(hDen) >= abs(vDen)) { + // Use a rather horizontal orientation, y = mx +p + isRatherVertical = false; + a = ((n * sxy) - (sx * sy)) / hDen; + b = -1d; + c = ((sy * sx2) - (sx * sxy)) / hDen; + } else { + // Use a rather vertical orientation, x = my +p + isRatherVertical = true; + a = -1d; + b = ((n * sxy) - (sx * sy)) / vDen; + c = ((sx * sy2) - (sy * sxy)) / vDen; + } + + normalize(); + dirty = false; + } + + //-----------// + // normalize // + //-----------// + /** + * Compute the distance normalizing factor + */ + private void normalize () + { + double norm = hypot(a, b); + a /= norm; + b /= norm; + c /= norm; + } +} diff --git a/src/main/omr/math/Circle.java b/src/main/omr/math/Circle.java new file mode 100644 index 0000000..0fc9a38 --- /dev/null +++ b/src/main/omr/math/Circle.java @@ -0,0 +1,579 @@ +//----------------------------------------------------------------------------// +// // +// C i r c l e // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.math; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import Jama.Matrix; + +import java.awt.geom.CubicCurve2D; +import java.awt.geom.Line2D; +import java.awt.geom.Point2D; +import static java.lang.Math.*; +import java.util.ArrayList; + +/** + * Class {@code Circle} handles a circle (or a portion of circle) + * which approximates a collection of data points. + * Besides usual characteristics of a circle (center, radius), and of a circle + * arc (start and stop angles) it also defines the approximating bezier curve. + * + * @author Hervé Bitteur + */ +public class Circle +{ + //~ Static fields/initializers --------------------------------------------- + + /** Usual logger utility */ + private static final Logger logger = LoggerFactory.getLogger(Circle.class); + + /** Size for matrices used to compute the circle */ + private static final int dim = 4; + + //~ Instance fields -------------------------------------------------------- + /** Mean algebraic distance between circle and the defining points */ + private final double distance; + + // Circle characteristics + /** Center */ + private Point2D.Double center; + + /** Radius */ + private Double radius; + + /** Starting limit of circle arc */ + private Double startAngle; + + /** Stopping limit of circle arc */ + private Double stopAngle; + + /** Bezier curve for circle arc */ + private CubicCurve2D.Double curve; + + // Bounding coordinates + private double xMax = Double.MIN_VALUE; + + private double yMax = Double.MIN_VALUE; + + private double xMin = Double.MAX_VALUE; + + private double yMin = Double.MAX_VALUE; + + //~ Constructors ----------------------------------------------------------- + //--------// + // Circle // + //--------// + /** + * Creates a new instance of Circle, defined by a set of points. + * + * @param x array of abscissae + * @param y array of ordinates + */ + public Circle (double[] x, + double[] y) + { + fit(x, y); + computeAngles(x, y); + distance = computeDistance(x, y); + } + + //--------// + // Circle // + //--------// + /** + * Creates a new instance of Circle, fitted to 3 defining points. + * The provided collection of coordinates is used only to compute the + * resulting distance. + * + * @param left left defining point + * @param middle middle defining point + * @param right right defining point + * @param x array of abscissae (including the defining points) + * @param y array of ordinates (including the defining points) + */ + public Circle (Point2D left, + Point2D middle, + Point2D right, + double[] x, + double[] y) + { + defineCircle(left, middle, right); + computeAngles(x, y); + distance = computeDistance(x, y); + } + + //~ Methods ---------------------------------------------------------------- + //-----------// + // getCenter // + //-----------// + /** + * Report the circle center. + * + * @return the center of the circle + */ + public Point2D.Double getCenter () + { + return center; + } + + //----------// + // getCurve // + //----------// + /** + * Report the Bezier curve which best approximates the circle arc. + * + * @return the Bezier curve + */ + public CubicCurve2D.Double getCurve () + { + if (curve == null) { + computeCurve(); + } + + return curve; + } + + //-------------// + // getDistance // + //-------------// + /** + * Report the mean distance between the data points and the circle. + * + * @return the mean distance + */ + public double getDistance () + { + return distance; + } + + //-----------// + // getRadius // + //-----------// + /** + * Report the circle radius. + * + * @return the circle radius + */ + public Double getRadius () + { + return radius; + } + + //---------------// + // getStartAngle // + //---------------// + /** + * Report the angle at start of the circle arc. + * + * @return the starting angle, in radians + */ + public Double getStartAngle () + { + return startAngle; + } + + //--------------// + // getStopAngle // + //--------------// + /** + * Report the angle at stop of the circle arc. + * + * @return the stopping angle, in radians + */ + public Double getStopAngle () + { + return stopAngle; + } + + //----------// + // toString // + //----------// + @Override + public String toString () + { + StringBuilder sb = new StringBuilder(); + + sb.append("{Circle"); + sb.append(String.format(" dist=%g", distance)); + sb.append(String.format(" center[%g,%g]", center.x, center.y)); + sb.append(String.format(" radius=%g", radius)); + + if ((startAngle != null) && (stopAngle != null)) { + sb.append( + String.format( + " angles=(%g,%g)", + toDegrees(startAngle), + toDegrees(stopAngle))); + } + + sb.append("}"); + + return sb.toString(); + } + + //---------------// + // computeAngles // + //---------------// + /** + * Compute the start and stop angles of a circle. + */ + private void computeAngles (double[] x, + double[] y) + { + // Get all angles, split into buckets + final int BUCKET_NB = 8; + final int[] buckets = new int[BUCKET_NB]; + + for (int i = 0; i < BUCKET_NB; i++) { + buckets[i] = 0; + } + + final double bucketSize = (2 * PI) / BUCKET_NB; + ArrayList angles = new ArrayList<>(); + + for (int i = 0; i < x.length; i++) { + // Get an angle between 0 and 2*PI + double angle = PI + atan2(y[i] - center.y, x[i] - center.x); + angles.add(angle); + + int idx = (int) (angle / bucketSize); + + if ((idx >= 0) && (idx < BUCKET_NB)) { + buckets[idx] += 1; + } + } + + // Find an empty bucket + int emptyIdx; + + for (emptyIdx = 0; emptyIdx < BUCKET_NB; emptyIdx++) { + if (buckets[emptyIdx] == 0) { + break; + } + } + + if (emptyIdx >= BUCKET_NB) { + logger.debug("No empty sector in circle, this is not a slur"); + } else { + final double bottom = emptyIdx * bucketSize; + double start = 2 * PI; + double stop = 0; + + for (double angle : angles) { + angle -= bottom; + + if (angle < 0) { + angle += (2 * PI); + } + + if (angle < start) { + start = angle; + } + + if (angle > stop) { + stop = angle; + } + } + + stop += (bottom - PI); + start += (bottom - PI); + + if (stop < start) { + stop += (2 * PI); + } + + startAngle = start; + stopAngle = stop; + + // System.out.println( + // "emptyIdx=" + emptyIdx + " startDeg=" + + // (float) toDegrees(start) + " stopDeg=" + + // (float) toDegrees(stop)); + } + } + + //--------------// + // computeCurve // + //--------------// + /** + * Compute the bezier points for the circle arc. + */ + private void computeCurve () + { + // Make sure we do have an arc defined, rather than a full circle + if (((stopAngle == null) || (stopAngle.isNaN())) + || ((startAngle == null) || (startAngle.isNaN()))) { + return; + } + + // Bezier points for circle arc, centered at origin, with radius 1 + double arc = stopAngle - startAngle; + double x0 = cos(arc / 2); + double y0 = sin(arc / 2); + double x1 = (4 - x0) / 3; + double y1 = ((1 - x0) * (3 - x0)) / (3 * y0); + double x2 = x1; + double y2 = -y1; + double x3 = x0; + double y3 = -y0; + + // Rotation + final double theta = (startAngle + stopAngle) / 2; + + ///System.out.println("angleDeg/2=" + Math.toDegrees(theta)); + final Matrix rotation = new Matrix( + new double[][]{ + {cos(theta), -sin(theta), 0}, + {sin(theta), cos(theta), 0}, + {0, 0, 1} + }); + + // Scaling + final Matrix scaling = new Matrix( + new double[][]{ + {radius, 0, 0}, + {0, radius, 0}, + {0, 0, 1} + }); + + // Translation + final Matrix translation = new Matrix( + new double[][]{ + {1, 0, center.x}, + {0, 1, center.y}, + {0, 0, 1} + }); + + // Composite operation + final Matrix op = translation.times(scaling).times(rotation); + + final Matrix M0 = op.times( + new Matrix( + new double[][]{ + {x0}, + {y0}, + {1} + })); + + final Matrix M1 = op.times( + new Matrix( + new double[][]{ + {x1}, + {y1}, + {1} + })); + + final Matrix M2 = op.times( + new Matrix( + new double[][]{ + {x2}, + {y2}, + {1} + })); + + final Matrix M3 = op.times( + new Matrix( + new double[][]{ + {x3}, + {y3}, + {1} + })); + + // Bezier curve (make sure the curve goes from left to right) + if (M0.get(0, 0) <= M3.get(0, 0)) { + curve = new CubicCurve2D.Double( + M0.get(0, 0), + M0.get(1, 0), + M1.get(0, 0), + M1.get(1, 0), + M2.get(0, 0), + M2.get(1, 0), + M3.get(0, 0), + M3.get(1, 0)); + } else { + curve = new CubicCurve2D.Double( + M3.get(0, 0), + M3.get(1, 0), + M2.get(0, 0), + M2.get(1, 0), + M1.get(0, 0), + M1.get(1, 0), + M0.get(0, 0), + M0.get(1, 0)); + } + + // System.out.println(" P1=" + curve.getP1()); + // System.out.println("CP1=" + curve.getCtrlP1()); + // System.out.println("CP2=" + curve.getCtrlP2()); + // System.out.println(" P2=" + curve.getP2()); + } + + //-----------------// + // computeDistance // + //-----------------// + /** + * Compute the mean quadratic distance of all points to the circle. + * + * @param x array of abscissae + * @param y array of ordinates + * @return the mean quadratic distance + */ + private double computeDistance (double[] x, + double[] y) + { + final int nbPoints = x.length; + double sum = 0; + + for (int i = 0; i < nbPoints; i++) { + double delta = Math.hypot( + x[i] - getCenter().x, + y[i] - getCenter().y) - getRadius(); + sum += (delta * delta); + } + + return Math.sqrt(sum) / nbPoints; + } + + //--------------// + // defineCircle // + //--------------// + /** + * Define the circle by means of a sequence of 3 key points. + * + * @param left precise left point + * @param middle a point rather in the middle + * @param right precise right point + */ + private void defineCircle (Point2D left, + Point2D middle, + Point2D right) + { + Line2D prevBisector = LineUtil.bisector( + new Line2D.Double(left, middle)); + Line2D bisector = LineUtil.bisector( + new Line2D.Double(middle, right)); + center = LineUtil.intersection( + prevBisector.getP1(), + prevBisector.getP2(), + bisector.getP1(), + bisector.getP2()); + radius = Math.hypot( + center.getX() - right.getX(), + center.getY() - right.getY()); + } + + //-----// + // fit // + //-----// + /** + * Given a collection of points, determine the best approximating + * circle. + * The result is available in the center and radius variables. + * + * @param x the array of abscissae + * @param y the array of ordinates + */ + private void fit (double[] x, + double[] y) + { + // Target is to minimize sum(||a(x2+y2) +dx +ey +f||) w/ constraint a=1 + // That is DV, w/ D=Design matrix, and V= [a d e f] + // a = 1 can be written CV = 1, w/ C = [1 0 0 0] + // Function to minimize is V'D'DV - lambda*(CV -1) + // w/ lambda as the Lagrange multiplier + // At the extremum, the gradient of this function is nul, so: + // 2D'DV -lambda.C' = 0 and a=1 + + /** number of points */ + int nbPoints = x.length; + + if (nbPoints < 3) { + throw new IllegalArgumentException("Less than 3 defining points"); + } + + // Build the design matrix, and remember the bounding box + Matrix design = new Matrix(nbPoints, dim); + + for (int i = 0; i < nbPoints; i++) { + final double tx = x[i]; + final double ty = y[i]; + design.set(i, 0, (tx * tx) + (ty * ty)); + design.set(i, 1, tx); + design.set(i, 2, ty); + design.set(i, 3, 1); + + if (tx > xMax) { + xMax = tx; + } + + if (tx < xMin) { + xMin = tx; + } + + if (ty > yMax) { + yMax = ty; + } + + if (ty < yMin) { + yMin = ty; + } + } + + ///print(design, "design"); + + // Build the scatter matrix + Matrix scatter = design.transpose().times(design); + + ///print(scatter, "scatter"); + + // Let's impose A = 1 + // So let's swap the first column with the Lambda.C' column + Matrix first = new Matrix(dim, 1); + + for (int i = 0; i < dim; i++) { + first.set(i, 0, -scatter.get(0, i)); + } + + Matrix newScatter = new Matrix(dim, dim); + + for (int i = 0; i < dim; i++) { + for (int j = 1; j < dim; j++) { + newScatter.set(i, j - 1, scatter.get(i, j)); + } + + newScatter.set(i, dim - 1, 0.0); + } + + newScatter.set(0, dim - 1, -0.5); + + ///print(newScatter, "newScatter"); + + // Solution [D E F lambda] + Matrix newScatterInv = newScatter.inverse(); + + ///print(newScatterInv, "newScatterInv"); + Matrix Solution = newScatterInv.times(first); + + ///print(Solution, "Solution [D E F Lambda]"); + + // Coefficients of the algebraic equation + // x**2 + y**2 + D*x + E*y + F = 0 + double D = Solution.get(0, 0); + double E = Solution.get(1, 0); + double F = Solution.get(2, 0); + + // Compute center & radius + center = new Point2D.Double(-D / 2, -E / 2); + radius = Math.sqrt(((center.x * center.x) + (center.y * center.y)) - F); + } +} diff --git a/src/main/omr/math/Ellipse.java b/src/main/omr/math/Ellipse.java new file mode 100644 index 0000000..2e74abe --- /dev/null +++ b/src/main/omr/math/Ellipse.java @@ -0,0 +1,609 @@ +//----------------------------------------------------------------------------// +// // +// E l l i p s e // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.math; + +import Jama.EigenvalueDecomposition; +import Jama.Matrix; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.awt.geom.Point2D; +import static java.lang.Math.*; + +/** + * Class {@code Ellipse} implements the direct algorithm of Fitzgibbon + * et al, improved by Halir et al, to find the ellipse which best + * approximates a collection of points. + * The ellipse is defined through the 6 coefficients of its algebraic equation: + * + *

A*x**2 + B*x*y + C*y**2 + D*x + E*y + F = 0 + * + *

It can also compute the ellipse characteristics (center, theta, major, + * minor) from its algebraic equation. + * + * @author Hervé Bitteur + */ +public class Ellipse +{ + //~ Static fields/initializers --------------------------------------------- + + /** Usual logger utility */ + private static final Logger logger = LoggerFactory.getLogger(Ellipse.class); + + /** Contraint such that 4*A*C - B**2 = 1 */ + private static final Matrix C1 = new Matrix( + new double[][]{ + {0, 0, 2}, + {0, -1, 0}, + {2, 0, 0} + }); + + /** Inverse of Constraint */ + private static final Matrix C1inv = new Matrix( + new double[][]{ + {0, 0, 0.5}, + {0, -1, 0}, + {0.5, 0, 0} + }); + + /** Epsilon value for vertical or horizontal ellipses */ + private static final double EPSILON = 1.0e-15; + + //~ Instance fields -------------------------------------------------------- + /** + * Array of coefficients that define ellipse algebraic equation + */ + protected double[] coeffs = new double[6]; + + /** Coefficient of x**2 */ + protected double A; + + /** Coefficient of x*y */ + protected double B; + + /** Coefficient of y**2 */ + protected double C; + + /** Coefficient of x */ + protected double D; + + /** Coefficient of y */ + protected double E; + + /** Coefficient of 1 */ + protected double F; + + /** Mean algebraic distance between ellipse and the defining points */ + protected double distance; + + // Ellipse characteristics + /** Center of ellipse */ + protected Point2D.Double center; + + /** Angle of main axis */ + protected Double angle; + + /** 1/2 Major axis */ + protected Double major; + + /** 1/2 Minor axis */ + protected Double minor; + + //~ Constructors ----------------------------------------------------------- + //---------// + // Ellipse // + //---------// + /** + * Creates a new instance of Ellipse, defined by a set of points + * + * @param x array of abscissae + * @param y array of ordinates + */ + public Ellipse (double[] x, + double[] y) + { + fit(x, y); + computeCharacteristics(); + } + + /** + * Creates a new Ellipse object. + */ + protected Ellipse () + { + } + + //~ Methods ---------------------------------------------------------------- + //----------// + // getAngle // + //----------// + /** + * Report the angle between the major axis and the abscissae axis + * + * @return the major axis angle, in radians + */ + public double getAngle () + { + if (angle == null) { + computeCharacteristics(); + } + + return angle; + } + + //-----------// + // getCenter // + //-----------// + /** + * Report the center of the ellipse, using the same coordinate system as the + * defining data points + * + * @return the ellipse center + */ + public Point2D.Double getCenter () + { + if (center == null) { + computeCharacteristics(); + } + + return center; + } + + //-----------------// + // getCoefficients // + //-----------------// + /** + * Report the coefficients of the ellipse, as defined by the algebraic + * equation + * + * @return the algebraic coefficients, all packed in one array + */ + public double[] getCoefficients () + { + return coeffs; + } + + //-------------// + // getDistance // + //-------------// + /** + * Report the mean algebraic distance between the data points and the + * ellipse + * + * @return the mean algebraic distance + */ + public double getDistance () + { + return distance; + } + + //----------// + // getMajor // + //----------// + /** + * Report the 1/2 length of the major axis + * + * @return the half major length + */ + public Double getMajor () + { + if (major == null) { + computeCharacteristics(); + } + + return major; + } + + //----------// + // getMinor // + //----------// + /** + * Report the 1/2 length of the minor axis + * + * @return the half minor length + */ + public Double getMinor () + { + if (minor == null) { + computeCharacteristics(); + } + + return minor; + } + + //-------// + // print // + //-------// + protected static void print (Matrix m, + String title) + { + StringBuilder sb = new StringBuilder(); + + if (title != null) { + sb.append(String.format("%s%n", title)); + } else { + sb.append(String.format("%n")); + } + + for (int row = 0; row < m.getRowDimension(); row++) { + sb.append(" "); + + for (int col = 0; col < m.getColumnDimension(); col++) { + sb.append(String.format("%15g ", m.get(row, col))); + } + + sb.append(String.format("%n")); + } + + sb.append(String.format("%n")); + + logger.info(sb.toString()); + } + + //---------------------// + // computeAngleAndAxes // + //---------------------// + /** + * Compute the angle (between -PI/2 and +PI/2) of the major axis, as well as + * the lengths of the major and minor axes. + */ + protected void computeAngleAndAxes () + { + if (abs(B) < EPSILON) { + if (A <= C) { + // Ellipse is horizontal + angle = 0d; + major = sqrt(1 / A); + minor = sqrt(1 / C); + } else { + // Ellipse is vertical + angle = PI / 2; + major = sqrt(1 / C); + minor = sqrt(1 / A); + } + } else { + // Angle (modulo PI/2) + double R = (C - A) / B; + double tg = R - sqrt((R * R) + 1); + angle = atan(tg); + + // Axes lengths + double P = (2 * tg) / (1 + (tg * tg)); + + if ((B / P) <= (-B / P)) { + major = sqrt(2 / ((A + C) + (B / P))); + minor = sqrt(2 / ((A + C) - (B / P))); + } else { + // Switch + major = sqrt(2 / ((A + C) - (B / P))); + minor = sqrt(2 / ((A + C) + (B / P))); + + if (angle < 0) { + angle += (PI / 2); + } else { + angle -= (PI / 2); + } + } + + ///System.out.println("R=" + R + " tg=" + tg + " P=" + P); + } + } + + //---------------// + // computeCenter // + //---------------// + /** + * Compute ellipse center, based on the equation parameters + * + * @return the ellipse center + */ + protected Point2D.Double computeCenter () + { + /** + * Let's consider the points where the ellipse is crossed by the + * vertical line located at abscissa x : We have an equation in y, of + * degree 2, governed by its discriminent. + * + * A*x**2 + B*x*y + C*y**2 + D*x + E*y + F = 0 becomes : + * + * C*y**2 + y*(B*x + E) + (Ax**2 + D*x + F) + * + * For the vertical tangents, we have a double root, thus with a null + * discriminent (which gives us the two x values of these tangents) + * + * (B*x + E)**2 - 4*C*(Ax**2 + D*x + F) = 0 + * + * Rewritten as an x-equation : + * + * (B**2 -4*A*C)*x**2 + (2*B*E - 4*C*D)*x + E**2 -4*C*F + * + * By symmetry, the ellipse center is right in the middle, so its + * abscissa is half of the sum of the two roots (-b/2a) : + * + * centerX = (2*C*D - B*E) / (B**2 -4*A*C) + * + * And a similar approach on horizontal tangents would give : + * + * centerY = (2*A*E - B*D) / (B**2 -4*A*C) + */ + double den = (B * B) - (4 * A * C); + double x = ((2 * C * D) - (B * E)) / den; + double y = ((2 * A * E) - (B * D)) / den; + + return new Point2D.Double(x, y); + } + + //--------------------------// + // computeCenterTranslation // + //--------------------------// + /** + * Perform a change in variables, by using the ellipse center as the system + * center. This nullifies parameters D and E. + */ + protected void computeCenterTranslation () + { + double x = center.x; + double y = center.y; + + // Perform translation to ellipse center + F += (((A * x * x) + (B * x * y) + (C * y * y)) + (D * x) + (E * y)); + //D += ((2 * A * x) + (B * y)); + D = 0; + //E += ((B * x) + (2 * C * y)); + E = 0; + + // Normalize + A /= -F; + B /= -F; + C /= -F; + F = -1; + + // Update the coeffs array accordingly + coeffs[0] = A; + coeffs[1] = B; + coeffs[2] = C; + coeffs[3] = D; + coeffs[4] = E; + coeffs[5] = F; + } + + //------------------------// + // computeCharacteristics // + //------------------------// + /** + * Compute the typical ellipse characteristic parameters (center, angle, + * major, minor) out of the algebraic coefficients. + */ + protected void computeCharacteristics () + { + System.out.println("-- computeCharacteristics"); + + // Compute ellipse center + center = computeCenter(); + + // Translate to ellipse center + computeCenterTranslation(); + + // Compute angle and axes + computeAngleAndAxes(); + } + + //-----// + // fit // + //-----// + /** + * Compute the algebraic parameters of the ellipse that best fits the + * provided set of data points. + * + * @param x the sequence of abscissae + * @param y the sequence of ordinates + */ + protected void fit (double[] x, + double[] y) + { + System.out.println("-- fit"); + + // Check input + if (x.length != y.length) { + throw new IllegalArgumentException( + "x & y arrays have different lengths"); + } + + if (x.length < 6) { + throw new IllegalArgumentException("Less than 6 defining points"); + } + + /** + * Contraint matrix C is decomposed in + * (C1 | 0 ) + * (---+---) + * ( 0 | 0 ) + * */ + ///print(C1, "C1"); + ///print(C1inv, "C1inv"); + /** number of points */ + int nbPoints = x.length; + + /** + * Design matrix D is decomposed in + * (D1|D2) + */ + Matrix D1 = new Matrix(nbPoints, 3); + Matrix D2 = new Matrix(nbPoints, 3); + + for (int i = 0; i < nbPoints; i++) { + final double tx = x[i]; + final double ty = y[i]; + D1.set(i, 0, tx * tx); + D1.set(i, 1, tx * ty); + D1.set(i, 2, ty * ty); + D2.set(i, 0, tx); + D2.set(i, 1, ty); + D2.set(i, 2, 1); + } + + ///print(D1, "D1"); + ///print(D2, "D2"); + + /** + * Scatter matrix S is decomposed in 4 matrices + * (S1 | S2) + * (---+---) + * (S2'| S3) + */ + /** S1 = D1'.D1 */ + Matrix S1 = D1.transpose() + .times(D1); + + /** S2 = D1'.D2 */ + Matrix S2 = D1.transpose() + .times(D2); + + /** S3 = D2'.D2 */ + Matrix S3 = D2.transpose() + .times(D2); + + ///print(S2, "S2"); + ///print(S1, "S1"); + ///print(S3, "S3"); + + /** + * Initial equation S.A = lambda.C.A can be rewritten : + * + * (S1 | S2) (A1) (C1 | 0 ) (A1) + * (---+---) . (--) = lambda . (---+---) . (--) + * (S2'| S3) (A2) ( 0 | 0 ) (A2) + * + * which is equivalent to : + * S1.A1 + S2.A2 = lambda.C1.A1 + * S2'.A1 + S3.A2 = 0 + * + * So + * A2 = -S3inv.S2'.A1 + * (S1 - S2.S3inv.S2').A1 = lambda.C1.A1 or + * C1inv.(S1 - S2.S3inv.S2').A1 = lambda.A1 + * + * Contraint is now + * A1'.C1.A1 = 1 + * + * w/ Reduced scatter matrix M = C1inv.S1 - S2.S3inv.S2' + * we now have : + * + * M.A1 = lambda.A1 + * A1'.C1.A1 = 1 + * A2 = -S3inv.S2'.A1 + */ + Matrix M = C1inv.times( + S1.minus(S2.times(S3.inverse()).times(S2.transpose()))); + + ///print(M, "M"); + + /** Retrieve eigen vectors and values for A1 */ + EigenvalueDecomposition ed = new EigenvalueDecomposition(M); + Matrix eigenVectors = ed.getV(); + double[] eigenValues = ed.getRealEigenvalues(); + + ///print(eigenVectors, "EigenVectors"); + ///System.out.println("EigenValues"); + ///for (double v : eigenValues) { + /// System.out.print(String.format(" %g", v)); + ///} + ///System.out.println(); + + /** + * Evaluate A1'.C1.A1 for each eigenvector, + * and keep the one with positive result + */ + Matrix A1; + double lambda = 0; + int index = 0; + + for (int i = 0; i < 3; i++) { + A1 = eigenVectors.getMatrix(0, 2, i, i); + + ///print(A1, "vector " + i); + Matrix R = A1.transpose() + .times(C1) + .times(A1); + + ///print(R, "R"); + if (R.get(0, 0) > 0) { + lambda = eigenValues[i]; + index = i; + } + } + + /** Copy the first 3 coefficients from A1 */ + A1 = eigenVectors.getMatrix(0, 2, index, index); + + print(A1, "A1"); + + for (int i = 0; i < 3; i++) { + coeffs[i] = A1.get(i, 0); + } + + /** Copy the 3 other coefficients from A2 = -S3inv.S2'.A1 */ + Matrix A2 = S3.inverse() + .times(S2.transpose()) + .times(A1) + .uminus(); + + print(A2, "A2"); + + for (int i = 0; i < 3; i++) { + coeffs[i + 3] = A2.get(i, 0); + } + + /** Store also coeffs as individual variables to ease formulae */ + A = coeffs[0]; // Nothing to do with matrix A + B = coeffs[1]; + C = coeffs[2]; + D = coeffs[3]; + E = coeffs[4]; + F = coeffs[5]; + + /** + * Compute the mean distance + * || D.A ||**2 = A'.D'.D.A = A'.S.A = lambda.A'.C.A = lambda + */ + if (lambda > 0) { + distance = Math.sqrt(lambda / nbPoints); + } else { + distance = 0; + } + ///System.out.println("lambda distance=" + distance); + // Let's try a brutal distance computation + { + Matrix D = new Matrix(nbPoints, 6); + D.setMatrix(0, nbPoints - 1, 0, 2, D1); + D.setMatrix(0, nbPoints - 1, 3, 5, D2); + + Matrix A = new Matrix(6, 1); + A.setMatrix(0, 2, 0, 0, A1); + A.setMatrix(3, 5, 0, 0, A2); + + Matrix DA = D.times(A); + double s = 0; + + for (int i = 0; i < nbPoints; i++) { + double val = DA.get(i, 0); + s += (val * val); + } + + s /= nbPoints; + distance = sqrt(s); + } + + ///System.out.println("distance: " + distance); + } +} diff --git a/src/main/omr/math/GCD.java b/src/main/omr/math/GCD.java new file mode 100644 index 0000000..48cd3c6 --- /dev/null +++ b/src/main/omr/math/GCD.java @@ -0,0 +1,147 @@ +//----------------------------------------------------------------------------// +// // +// G C D // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.math; + +import java.util.Collection; + +/** + * Class {@code GCD} gathers several functions to compute Greatest + * Common Divisor of a ensemble of integer values. + * + * @author Hervé Bitteur + */ +public class GCD +{ + //~ Constructors ----------------------------------------------------------- + + /** Not meant to be instantiated */ + private GCD () + { + } + + //~ Methods ---------------------------------------------------------------- + //-----// + // gcd // + //-----// + /** + * Report the gcd of an array of int values + * + * @param vals the array of int values + * @return the gcd over the int values + */ + public static int gcd (int[] vals) + { + int s = 0; + + for (int val : vals) { + s = gcd(s, val); + } + + return s; + } + + //-----// + // gcd // + //-----// + /** + * Basic gcd computation for 2 int values, assumed to be positive or zero + * + * @param m one int value + * @param n another int value + * @return the gcd of the two values + */ + public static int gcd (int m, + int n) + { + if (n == 0) { + return m; + } else { + return gcd(n, m % n); + } + } + + //-----// + // gcd // + //-----// + /** + * Report the gcd of a collection of integer values + * + * @param vals the collection of values + * @return the gcd over the collection + */ + public static int gcd (Collection vals) + { + return gcd(vals.toArray(new Integer[vals.size()])); + } + + //-----// + // gcd // + //-----// + /** + * Report the gcd of an array of integer values + * + * @param vals the array of integer values + * @return the gcd over the values + */ + public static int gcd (Integer[] vals) + { + int s = 0; + + for (int val : vals) { + s = gcd(s, val); + } + + return s; + } + + //-----// + // lcm // + //-----// + /** + * Report the Least Common Multiple of 2 values, assumed to be positive + * or zero + * + * @param m + * @param n + * @return lcm(|m|, |n|) + */ + public static int lcm (int m, + int n) + { + if (m < 0) { + m = -m; + } + + if (n < 0) { + n = -n; + } + + return m * (n / gcd(m, n)); + } + + //-----// + // lcm // + //-----// + /** + * Report the Least Common Multiple of n values + */ + public static int lcm (int... vals) + { + int s = vals[0]; + + for (int val : vals) { + s = lcm(s, val); + } + + return s; + } +} diff --git a/src/main/omr/math/GeoPath.java b/src/main/omr/math/GeoPath.java new file mode 100644 index 0000000..2897c6f --- /dev/null +++ b/src/main/omr/math/GeoPath.java @@ -0,0 +1,408 @@ +//----------------------------------------------------------------------------// +// // +// G e o P a t h // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.math; + +import java.awt.Shape; +import java.awt.geom.AffineTransform; +import java.awt.geom.Path2D; +import java.awt.geom.PathIterator; +import static java.awt.geom.PathIterator.*; +import java.awt.geom.Point2D; +import java.awt.geom.Rectangle2D; + +/** + * Class {@code GeoPath} is a Path2D.Double with some additions + * + * @author Hervé Bitteur + */ +public class GeoPath + extends Path2D.Double +{ + //~ Constructors ----------------------------------------------------------- + + //---------// + // GeoPath // + //---------// + /** + * Creates a new GeoPath object. + */ + public GeoPath () + { + } + + //---------// + // GeoPath // + //---------// + /** + * Creates a new GeoPath object. + * + * @param s the specified {@code Shape} object + */ + public GeoPath (Shape s) + { + this(s, null); + } + + //---------// + // GeoPath // + //---------// + /** + * Creates a new GeoPath object. + * + * @param s the specified {@code Shape} object + * @param at the specified {@code AffineTransform} object + */ + public GeoPath (Shape s, + AffineTransform at) + { + super(s, at); + } + + //~ Methods ---------------------------------------------------------------- + //---------// + // labelOf // + //---------// + /** + * Report the kind label of a segment. + * + * @param segmentKind the int-based segment kind + * @return the label for the curve + */ + public static String labelOf (int segmentKind) + { + switch (segmentKind) { + case SEG_MOVETO: + return "SEG_MOVETO"; + + case SEG_LINETO: + return "SEG_LINETO"; + + case SEG_QUADTO: + return "SEG_QUADTO"; + + case SEG_CUBICTO: + return "SEG_CUBICTO"; + + case SEG_CLOSE: + return "SEG_CLOSE"; + + default: + throw new RuntimeException("Illegal segmentKind " + segmentKind); + } + } + + //------------// + // intersects // + //------------// + /** + * Check whether the flattened path intersects the provided rectangle. + * + * @param rect the provided rectangle to check for intersection + * @param flatness maximum distance used for line segment approximation + * @return true if intersection found + */ + public boolean intersects (Rectangle2D rect, + double flatness) + { + final double[] buffer = new double[6]; + double x1 = 0; + double y1 = 0; + + for (PathIterator it = getPathIterator(null, flatness); !it.isDone(); + it.next()) { + int segmentKind = it.currentSegment(buffer); + int count = countOf(segmentKind); + final double x2 = buffer[count - 2]; + final double y2 = buffer[count - 1]; + + switch (segmentKind) { + case SEG_MOVETO: + x1 = x2; + y1 = y2; + + break; + + case SEG_LINETO: + + if (rect.intersectsLine(x1, y1, x2, y2)) { + return true; + } + + break; + + case SEG_CLOSE: + break; + + default: + throw new RuntimeException( + "Illegal segmentKind " + segmentKind); + } + } + + return false; + } + + //----------// + // toString // + //----------// + @Override + public String toString () + { + StringBuilder sb = new StringBuilder("{"); + sb.append(getClass().getSimpleName()); + + double[] buffer = new double[6]; + + for (PathIterator it = getPathIterator(null); !it.isDone(); + it.next()) { + int segmentKind = it.currentSegment(buffer); + + sb.append(" ") + .append(labelOf(segmentKind)) + .append("("); + + int coords = countOf(segmentKind); + boolean firstCoord = true; + + for (int ic = 0; ic < (coords - 1); ic += 2) { + if (!firstCoord) { + sb.append(","); + firstCoord = false; + } + + sb.append("[") + .append((float) buffer[ic]) + .append(",") + .append((float) buffer[ic + 1]) + .append("]"); + } + + sb.append(")"); + } + + sb.append("}"); + + return sb.toString(); + } + + //------// + // xAtY // + //------// + /** + * Report the abscissa value of the spline at provided ordinate + * (assuming true function) + * + * @param y the provided ordinate + * @return the abscissa value at this ordinate + */ + public double xAtY (double y) + { + final double[] buffer = new double[6]; + final Point2D.Double p1 = new Point2D.Double(); + final Point2D.Double p2 = new Point2D.Double(); + final int segmentKind = getYSegment(y, buffer, p1, p2); + final double t = (y - p1.y) / (p2.y - p1.y); + final double u = 1 - t; + + switch (segmentKind) { + case SEG_LINETO: + return p1.x + (t * (p2.x - p1.x)); + + case SEG_QUADTO: { + double cpx = buffer[0]; + + return (p1.x * u * u) + (2 * cpx * t * u) + (p2.x * t * t); + } + + case SEG_CUBICTO: { + double cpx1 = buffer[0]; + double cpx2 = buffer[2]; + + return (p1.x * u * u * u) + (3 * cpx1 * t * u * u) + + (3 * cpx2 * t * t * u) + (p2.x * t * t * t); + } + + default: + throw new RuntimeException("Illegal segmentKind " + segmentKind); + } + } + + //------// + // yAtX // + //------// + /** + * Report the ordinate value of the spline at provided abscissa + * (assuming true function) + * + * @param x the provided abscissa + * @return the ordinate value at this abscissa + */ + public double yAtX (double x) + { + final double[] buffer = new double[6]; + final Point2D.Double p1 = new Point2D.Double(); + final Point2D.Double p2 = new Point2D.Double(); + final int segmentKind = getXSegment(x, buffer, p1, p2); + final double t = (x - p1.x) / (p2.x - p1.x); + final double u = 1 - t; + + switch (segmentKind) { + case SEG_LINETO: + return p1.y + (t * (p2.y - p1.y)); + + case SEG_QUADTO: { + double cpy = buffer[1]; + + return (p1.y * u * u) + (2 * cpy * t * u) + (p2.y * t * t); + } + + case SEG_CUBICTO: { + double cpy1 = buffer[1]; + double cpy2 = buffer[3]; + + return (p1.y * u * u * u) + (3 * cpy1 * t * u * u) + + (3 * cpy2 * t * t * u) + (p2.y * t * t * t); + } + + default: + throw new RuntimeException("Illegal segmentKind " + segmentKind); + } + } + + //---------// + // countOf // + //---------// + /** + * Report how many coordinate values a path segment contains. + * + * @param segmentKind the int-based segment kind + * @return the number of coordinates values + */ + protected static int countOf (int segmentKind) + { + switch (segmentKind) { + case SEG_MOVETO: + case SEG_LINETO: + return 2; + + case SEG_QUADTO: + return 4; + + case SEG_CUBICTO: + return 6; + + case SEG_CLOSE: + return 0; + + default: + throw new RuntimeException("Illegal segmentKind " + segmentKind); + } + } + + //-------------// + // getXSegment // + //-------------// + /** + * Retrieve the first segment of the curve that contains the provided + * abscissa + * + * @param x the provided abscissa + * @param buffer output + * @param p1 output: start of segment + * @param p2 output: end of segment + * @return the segment kind + */ + protected int getXSegment (double x, + double[] buffer, + Point2D.Double p1, + Point2D.Double p2) + { + PathIterator it = getPathIterator(null); + double x1 = 0; + double y1 = 0; + + while (!it.isDone()) { + final int segmentKind = it.currentSegment(buffer); + final int count = countOf(segmentKind); + final double x2 = buffer[count - 2]; + final double y2 = buffer[count - 1]; + + if ((segmentKind == SEG_MOVETO) + || (segmentKind == SEG_CLOSE) + || (x > x2)) { + // Move to next segment + x1 = x2; + y1 = y2; + it.next(); + } else { + p1.x = x1; + p1.y = y1; + p2.x = x2; + p2.y = y2; + + return segmentKind; + } + } + + // Not found + throw new RuntimeException("Abscissa not in range: " + x); + } + + //-------------// + // getYSegment // + //-------------// + /** + * Retrieve the first segment of the curve that contains the provided + * ordinate + * + * @param y the provided ordinate + * @param buffer output + * @param p1 output: start of segment + * @param p2 output: end of segment + * @return the segment kind + */ + protected int getYSegment (double y, + double[] buffer, + Point2D.Double p1, + Point2D.Double p2) + { + PathIterator it = getPathIterator(null); + double x1 = 0; + double y1 = 0; + + while (!it.isDone()) { + final int segmentKind = it.currentSegment(buffer); + final int count = countOf(segmentKind); + final double x2 = buffer[count - 2]; + final double y2 = buffer[count - 1]; + + if ((segmentKind == SEG_MOVETO) + || (segmentKind == SEG_CLOSE) + || (y > y2)) { + // Move to next segment + x1 = x2; + y1 = y2; + it.next(); + } else { + p1.x = x1; + p1.y = y1; + p2.x = x2; + p2.y = y2; + + return segmentKind; + } + } + + // Not found + throw new RuntimeException("Ordinate not in range: " + y); + } +} diff --git a/src/main/omr/math/GeoUtil.java b/src/main/omr/math/GeoUtil.java new file mode 100644 index 0000000..093a3a5 --- /dev/null +++ b/src/main/omr/math/GeoUtil.java @@ -0,0 +1,55 @@ +//----------------------------------------------------------------------------// +// // +// G e o U t i l // +// // +//----------------------------------------------------------------------------// +// // +// Copyright © Herve 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. // +//----------------------------------------------------------------------------// +// +package omr.math; + +import java.awt.Point; +import java.awt.Rectangle; + +/** + * Class {@code GeoUtil} gathers simple utilities related to geometry. + * + * @author Hervé Bitteur + */ +public class GeoUtil +{ + //~ Methods ---------------------------------------------------------------- + + //----------// + // vectorOf // + //----------// + /** + * Report the vector that goes from 'from' point to 'to' point. + * + * @param from the origin point + * @param to the target point + * @return the vector from origin to target + */ + public static Point vectorOf (Point from, + Point to) + { + return new Point(to.x - from.x, to.y - from.y); + } + + //----------// + // centerOf // + //----------// + /** + * Report the center of the provided rectangle + * + * @param rect the provided rectangle + * @return the geometric rectangle center + */ + public static Point centerOf (Rectangle rect) + { + return new Point(rect.x + (rect.width / 2), rect.y + (rect.height / 2)); + } +} diff --git a/src/main/omr/math/Histogram.java b/src/main/omr/math/Histogram.java new file mode 100644 index 0000000..a423fcf --- /dev/null +++ b/src/main/omr/math/Histogram.java @@ -0,0 +1,757 @@ +//----------------------------------------------------------------------------// +// // +// H i s t o g r a m // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.math; + +import java.io.PrintStream; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import java.util.SortedMap; +import java.util.TreeMap; + +/** + * Class {@code Histogram} is an histogram implementation which handles + * integer counts in buckets, the buckets identities being values of + * type K. + * + * @param the precise type for histogram buckets + * + * @author Hervé Bitteur + */ +public class Histogram +{ + //~ Instance fields -------------------------------------------------------- + + /** To sort peaks by decreasing value */ + public final Comparator> reversePeakComparator = new Comparator>() + { + @Override + public int compare (PeakEntry e1, + PeakEntry e2) + { + // Put largest value first! + return Double.compare(e2.getValue(), e1.getValue()); + } + }; + + /** To sort double peaks by decreasing value */ + public final Comparator> reverseDoublePeakComparator = new Comparator>() + { + @Override + public int compare (PeakEntry e1, + PeakEntry e2) + { + // Put largest value first! + return Double.compare(e2.getValue(), e1.getValue()); + } + }; + + /** To sort double peaks by decreasing value */ + public final Comparator> reverseMaxComparator = new Comparator>() + { + @Override + public int compare (MaxEntry e1, + MaxEntry e2) + { + // Put largest value first! + return Double.compare(e2.getValue(), e1.getValue()); + } + }; + + /** + * Underlying map: + * - K for the type of entity to be accumulated + * - Integer for the cumulated number in each bucket + */ + protected final SortedMap map = new TreeMap<>(); + + /** Total count */ + protected int totalCount = 0; + + //~ Constructors ----------------------------------------------------------- + //-----------// + // Histogram // + //-----------// + /** + * Creates a new Histogram object, with no pre-defined range of buckets + */ + public Histogram () + { + } + + //-----------// + // Histogram // + //-----------// + /** + * Creates a new Histogram object, with pre-definition of the bucket range + * + * @param first the first bucket of the foreseen range + * @param last the last bucket of the foreseen range + */ + public Histogram (K first, + K last) + { + map.put(first, 0); + map.put(last, 0); + } + + //~ Methods ---------------------------------------------------------------- + //-----------// + // bucketSet // + //-----------// + public Set bucketSet () + { + return map.keySet(); + } + + //-------// + // clear // + //-------// + public void clear () + { + map.clear(); + totalCount = 0; + } + + //------------// + // dataString // + //------------// + public String dataString () + { + StringBuilder sb = new StringBuilder("["); + + boolean first = true; + + for (Map.Entry entry : entrySet()) { + sb.append( + String.format( + "%s%s:%d", + first ? "" : " ", + entry.getKey().toString(), + entry.getValue())); + first = false; + } + + sb.append("]"); + + return sb.toString(); + } + + //----------// + // entrySet // + //----------// + public Set> entrySet () + { + return map.entrySet(); + } + + //-------------// + // firstBucket // + //-------------// + public K firstBucket () + { + return map.firstKey(); + } + + //----------// + // getCount // + //----------// + /** + * Report the count of specified bucket + * + * @param bucket the bucket of interest + * @return the bucket count (zero for any empty bucket) + */ + public int getCount (K bucket) + { + Integer count = map.get(bucket); + + if (count == null) { + return 0; + } else { + return count; + } + } + + //----------// + // getPeaks // + //----------// + /** + * Report the sequence of bucket peaks whose count is equal to or + * greater than the specified minCount value. + * + * @param minCount the desired minimum count value + * @return the (perhaps empty but not null) sequence of peaks of buckets + */ + public List> getDoublePeaks (int minCount) + { + final List> peaks = new ArrayList<>(); + K start = null; + K stop = null; + K best = null; + Integer bestCount = null; + boolean isAbove = false; + + for (Entry entry : map.entrySet()) { + if (entry.getValue() >= minCount) { + if ((bestCount == null) || (bestCount < entry.getValue())) { + best = entry.getKey(); + bestCount = entry.getValue(); + } + + if (isAbove) { // Above -> Above + stop = entry.getKey(); + } else { // Below -> Above + stop = start = entry.getKey(); + isAbove = true; + } + } else { + if (isAbove) { // Above -> Below + peaks.add( + new PeakEntry<>( + createDoublePeak(start, best, stop, minCount), + (double) bestCount / totalCount)); + stop = start = best = null; + bestCount = null; + isAbove = false; + } else { // Below -> Below + } + } + } + + // Last range + if (isAbove) { + peaks.add( + new PeakEntry<>( + createDoublePeak(start, best, stop, minCount), + (double) bestCount / totalCount)); + } + + // Sort by decreasing count values + Collections.sort(peaks, reverseDoublePeakComparator); + + return peaks; + } + + //----------------// + // getLocalMaxima // + //----------------// + /** + * Report the local maximum points, sorted by decreasing count + * + * @return the (count-based) sorted sequence of local maxima + */ + public List> getLocalMaxima () + { + final List> maxima = new ArrayList<>(); + K prevKey = null; + int prevValue = 0; + boolean growing = false; + + for (Entry entry : map.entrySet()) { + K key = entry.getKey(); + int value = entry.getValue(); + + if (prevKey != null) { + if (value >= prevValue) { + growing = true; + } else { + if (growing) { + // End of a local max + maxima.add( + new MaxEntry<>( + prevKey, + prevValue / (double) totalCount)); + } + + growing = false; + } + } + + prevKey = key; + prevValue = value; + } + + // Sort by decreasing count values + Collections.sort(maxima, reverseMaxComparator); + + return maxima; + } + + //--------------// + // getMaxBucket // + //--------------// + /** + * Report the bucket with highest count + * + * @return the most popular bucket + */ + public K getMaxBucket () + { + int max = Integer.MIN_VALUE; + K bucket = null; + + for (Map.Entry entry : map.entrySet()) { + if (entry.getValue() > max) { + max = entry.getValue(); + bucket = entry.getKey(); + } + } + + return bucket; + } + + //-------------// + // getMaxCount // + //-------------// + /** + * Report the highest count among all buckets + * + * @return the largest count value + */ + public int getMaxCount () + { + int max = Integer.MIN_VALUE; + + for (Map.Entry entry : map.entrySet()) { + max = Math.max(max, entry.getValue()); + } + + return max; + } + + //------------// + // getMaximum // + //------------// + /** + * Report the maximum entry in this histogram + * + * @return the maximum entry (key & value) + */ + public Map.Entry getMaximum () + { + Map.Entry maximum = null; + + for (Map.Entry entry : map.entrySet()) { + int value = entry.getValue(); + + if ((maximum == null) || (value > maximum.getValue())) { + maximum = entry; + } + } + + return maximum; + } + + //----------// + // getPeaks // + //----------// + /** + * Report the sequence of bucket peaks whose count is equal to or greater + * than the specified minCount value + * + * @param minCount the desired minimum count value + * @param absolute if true, absolute counts values are reported in peaks, + * otherwise relative counts to total histogram are used + * @param sorted if true, the reported sequence is sorted by decreasing + * count value, otherwise it is reported as naturally found along K data. + * @return the (perhaps empty but not null) sequence of peaks of buckets + */ + public List> getPeaks (int minCount, + boolean absolute, + boolean sorted) + { + final List> peaks = new ArrayList<>(); + K start = null; + K stop = null; + K best = null; + Integer bestCount = null; + boolean isAbove = false; + + for (Entry entry : map.entrySet()) { + if (entry.getValue() >= minCount) { + if ((bestCount == null) || (bestCount < entry.getValue())) { + best = entry.getKey(); + bestCount = entry.getValue(); + } + + if (isAbove) { // Above -> Above + stop = entry.getKey(); + } else { // Below -> Above + stop = start = entry.getKey(); + isAbove = true; + } + } else { + if (isAbove) { // Above -> Below + peaks.add( + new PeakEntry<>( + new Peak<>(start, best, stop), + absolute ? bestCount : ((double) bestCount / totalCount))); + stop = start = best = null; + bestCount = null; + isAbove = false; + } else { // Below -> Below + } + } + } + + // Last range + if (isAbove) { + peaks.add( + new PeakEntry<>( + new Peak<>(start, best, stop), + absolute ? bestCount : ((double) bestCount / totalCount))); + } + + // Sort by decreasing count values? + if (sorted) { + Collections.sort(peaks, reversePeakComparator); + } + + return peaks; + } + + //----------------// + // getQuorumValue // + //----------------// + /** + * Based on the current population, report the quorum value coresponding + * to the provided quorum ratio + * + * @param quorumRatio quorum specified as a percentage of total count + * @return the quorum value + */ + public int getQuorumValue (double quorumRatio) + { + return (int) Math.rint(quorumRatio * getTotalCount()); + } + + //---------------// + // getTotalCount // + //---------------// + /** + * Report the total counts of all buckets + * + * @return the sum of all counts + */ + public int getTotalCount () + { + return totalCount; + } + + //---------------// + // increaseCount // + //---------------// + public void increaseCount (K bucket, + int delta) + { + Integer count = map.get(bucket); + + if (count == null) { + map.put(bucket, delta); + } else { + map.put(bucket, count + delta); + } + + totalCount += delta; + } + + //------------// + // lastBucket // + //------------// + public K lastBucket () + { + return map.lastKey(); + } + + //-------// + // print // + //-------// + public void print (PrintStream stream) + { + stream.println(dataString()); + } + + //------// + // size // + //------// + /** + * Report the number of non empty buckets + * + * @return the number of non empty buckets + */ + public int size () + { + return map.size(); + } + + //----------// + // toString // + //----------// + @Override + public String toString () + { + StringBuilder sb = new StringBuilder("{"); + sb.append(getClass().getSimpleName()); + sb.append( + String.format( + " %s-%s", + (firstBucket() != null) ? firstBucket().toString() : "", + (lastBucket() != null) ? lastBucket().toString() : "")); + sb.append(" size:") + .append(size()); + + sb.append(" ") + .append(dataString()); + + sb.append("}"); + + return sb.toString(); + } + + //--------// + // values // + //--------// + public Collection values () + { + return map.values(); + } + + //------------------// + // createDoublePeak // + //------------------// + private DoublePeak createDoublePeak (K first, + K best, + K second, + int count) + { + // Use interpolation for more accurate data on first & second + double preciseFirst = first.doubleValue(); + K prevKey = prevKey(first); + + if (prevKey != null) { + preciseFirst = preciseKey(prevKey, first, count); + } + + double preciseSecond = second.doubleValue(); + K nextKey = nextKey(second); + + if (nextKey != null) { + preciseSecond = preciseKey(second, nextKey, count); + } + + return new DoublePeak(preciseFirst, best.doubleValue(), preciseSecond); + } + + //---------// + // nextKey // + //---------// + private K nextKey (K key) + { + boolean found = false; + + for (K k : map.keySet()) { + if (found) { + return k; + } else if (key.equals(k)) { + found = true; + } + } + + return null; + } + + //------------// + // preciseKey // + //------------// + private double preciseKey (K prev, + K next, + int count) + { + // Use interpolation for accurate data between prev & next keys + double prevCount = getCount(prev); + double nextCount = getCount(next); + + return ((prev.doubleValue() * (nextCount - count)) + + (next.doubleValue() * (count - prevCount))) / (nextCount + - prevCount); + } + + //---------// + // prevKey // + //---------// + private K prevKey (K key) + { + K prev = null; + + for (K k : map.keySet()) { + if (key.equals(k)) { + return prev; + } else { + prev = k; + } + } + + return null; + } + + //~ Inner Classes ---------------------------------------------------------- + //------------// + // DoublePeak // + //------------// + public static class DoublePeak + extends Peak + { + //~ Constructors ------------------------------------------------------- + + private DoublePeak (double first, + double best, + double second) + { + super(first, best, second); + } + } + + //----------// + // MaxEntry // + //----------// + public static class MaxEntry + { + //~ Instance fields ---------------------------------------------------- + + /** Key at local maximum */ + private final K key; + + /** Related count (normalized by total histogram count) */ + private final double value; + + //~ Constructors ------------------------------------------------------- + public MaxEntry (K key, + double value) + { + this.key = key; + this.value = value; + } + + //~ Methods ------------------------------------------------------------ + /** + * @return the key + */ + public K getKey () + { + return key; + } + + /** + * @return the value + */ + public double getValue () + { + return value; + } + + @Override + public String toString () + { + return getKey() + "=" + (float) getValue(); + } + } + + //------// + // Peak // + //------// + public static class Peak + { + //~ Instance fields ---------------------------------------------------- + + /** Value at beginning of range */ + public final K first; + + /** Value at highest point in range */ + public final K best; + + /** Value at end of range */ + public final K second; + + //~ Constructors ------------------------------------------------------- + public Peak (K first, + K best, + K second) + { + this.first = first; + this.best = best; + this.second = second; + } + + //~ Methods ------------------------------------------------------------ + @Override + public String toString () + { + return "(" + first.floatValue() + "," + best.floatValue() + "," + + second.floatValue() + ")"; + } + } + + //-----------// + // PeakEntry // + //-----------// + public static class PeakEntry + { + //~ Instance fields ---------------------------------------------------- + + /** The peak data */ + private final Peak key; + + /** Count at best value (normalized by total histogram count) */ + private final double value; + + //~ Constructors ------------------------------------------------------- + public PeakEntry (Peak key, + double value) + { + this.key = key; + this.value = value; + } + + //~ Methods ------------------------------------------------------------ + /** + * Returns the key. + * + * @return the key + */ + public Peak getKey () + { + return key; + } + + /** + * Returns the value associated with the key. + * + * @return the value associated with the key + */ + public double getValue () + { + return value; + } + + @Override + public String toString () + { + return key + "=" + (float) value; + } + } +} diff --git a/src/main/omr/math/InjectionSolver.java b/src/main/omr/math/InjectionSolver.java new file mode 100644 index 0000000..2b7a855 --- /dev/null +++ b/src/main/omr/math/InjectionSolver.java @@ -0,0 +1,161 @@ +//----------------------------------------------------------------------------// +// // +// I n j e c t i o n S o l v e r // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.math; + +import java.util.Arrays; + +/** + * Class {@code InjectionSolver} handles the injection of a collection + * of elements (called domain) into another collection of elements + * (called range, or codomain). + * + *

It finds a mapping that minimizes the global mapping distance, given + * the individual distance for each domain/range elements pair. This + * implementation uses brute force, and thus should be used with small + * sizes only. + * + * @author Hervé Bitteur + */ +public class InjectionSolver +{ + //~ Instance fields -------------------------------------------------------- + + private final int domainSize; + + private final int rangeSize; + + private final Distance distance; + + private final boolean[] free; + + private int bestCost = Integer.MAX_VALUE; + + private final int[] bestConfig; + + private final int[] config; + + //~ Constructors ----------------------------------------------------------- + /** + * Creates a new instance of InjectionSolver + * + * @param domainSize size of the domain collection + * @param rangeSize size of the range collection + * @param distance + */ + public InjectionSolver (int domainSize, + int rangeSize, + Distance distance) + { + // Parameters of the solver + this.domainSize = domainSize; + this.rangeSize = rangeSize; + this.distance = distance; + + free = new boolean[rangeSize]; + bestConfig = new int[domainSize]; + config = new int[domainSize]; + + // System.out.println( + // "InjectionSolver domainSize=" + domainSize + " rangeSize=" + + // rangeSize); + } + + //~ Methods ---------------------------------------------------------------- + //-------// + // solve // + //-------// + /** + * Report (one of) the mapping(s) for which the global distance is + * minimum. + * + * @return an array parallel to the domain collection, which for each + * (domain) element gives the mapped range element + */ + public int[] solve () + { + Arrays.fill(free, true); + inspect(0, 0); + + return bestConfig; + } + + //------// + // dump // + //------// + private void dump () + { + StringBuilder sb = new StringBuilder(); + sb.append("bestCost=") + .append(bestCost); + sb.append(" ["); + + for (int i = 0; i < bestConfig.length; i++) { + sb.append(" ") + .append(bestConfig[i]); + } + + sb.append("]"); + + System.out.println(sb.toString()); + } + + //---------// + // inspect // + //---------// + private void inspect (int id, + int cost) + { + // System.out.println("inspect id=" + id + " cost=" + cost); + for (int ir = 0; ir < rangeSize; ir++) { + if (free[ir]) { + free[ir] = false; + config[id] = ir; + + int newCost = cost + distance.getDistance(id, ir); + + /// System.out.println("ir=" + ir + " newCost=" + newCost); + if (id < (domainSize - 1)) { + inspect(id + 1, newCost); + } else if (newCost < bestCost) { + // Record best config so far + System.arraycopy(config, 0, bestConfig, 0, domainSize); + bestCost = newCost; + + // dump(); + } + + free[ir] = true; + } + } + } + + //~ Inner Interfaces ------------------------------------------------------- + /** + * Interface {@code Distance} provides the measurement for + * individual mapping costs. + */ + public static interface Distance + { + //~ Methods ------------------------------------------------------------ + + /** + * Report the distance when mapping element 'id' of domain to + * element 'ir' of range + * + * @param id index of domain element + * @param ir index of range element + * @return the cost of mapping these two elements + */ + int getDistance (int id, + int ir); + } +} diff --git a/src/main/omr/math/IntegerHistogram.java b/src/main/omr/math/IntegerHistogram.java new file mode 100644 index 0000000..de106f1 --- /dev/null +++ b/src/main/omr/math/IntegerHistogram.java @@ -0,0 +1,115 @@ +//----------------------------------------------------------------------------// +// // +// I n t e g e r H i s t o g r a m // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.math; + +import java.io.PrintStream; +import java.util.Map; +import java.util.SortedMap; +import java.util.TreeMap; + +/** + * Class {@code IntegerHistogram} is an histogram where buckets are + * integers. + * + * @author Hervé Bitteur + */ +public class IntegerHistogram + extends Histogram +{ + //~ Instance fields -------------------------------------------------------- + + /** The derivatives values */ + private SortedMap derivatives; + + //~ Methods ---------------------------------------------------------------- + //-------// + // clear // + //-------// + @Override + public void clear () + { + super.clear(); + derivatives = null; + } + + //----------------// + // getDerivatives // + //----------------// + public SortedMap getDerivatives () + { + if (derivatives == null) { + derivatives = new TreeMap<>(); + + Integer prevKey = null; + Integer prevValue = null; + Integer key = null; + Integer value = null; + + for (Map.Entry entry : map.entrySet()) { + Integer nextKey = entry.getKey(); + Integer nextValue = entry.getValue(); + + if (key != null) { + if (prevKey != null) { + // We can compute a derivative + derivatives.put( + key, + (double) (nextValue - prevValue) / (nextKey + - prevKey)); + } + + prevKey = key; + prevValue = value; + } + + key = nextKey; + value = nextValue; + } + } + + return derivatives; + } + + //---------------// + // increaseCount // + //---------------// + @Override + public void increaseCount (Integer bucket, + int delta) + { + super.increaseCount(bucket, delta); + derivatives = null; + } + + //-------// + // print // + //-------// + @Override + public void print (PrintStream stream) + { + stream.print("[\n"); + + getDerivatives(); + + for (Map.Entry entry : entrySet()) { + Integer key = entry.getKey(); + Double der = derivatives.get(key); + stream.format( + " %s: v:%d d:%s\n", + key.toString(), + entry.getValue(), + (der != null) ? der.toString() : ""); + } + + stream.println("]"); + } +} diff --git a/src/main/omr/math/Line.java b/src/main/omr/math/Line.java new file mode 100644 index 0000000..d9aeb07 --- /dev/null +++ b/src/main/omr/math/Line.java @@ -0,0 +1,182 @@ +//----------------------------------------------------------------------------// +// // +// L i n e // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.math; + +/** + * Interface {@code Line} handles the equation of a line (or more + * generally some curved line for which Y can be computed from X), + * whatever its orientation. + * + * @author Hervé Bitteur + */ +public interface Line +{ + //~ Methods ---------------------------------------------------------------- + + /** + * Compute the orthogonal distance between the line and the + * provided point. + * Note that the distance may be negative. + * + * @param x the point abscissa + * @param y the point ordinate + * @return the algebraic orthogonal distance + */ + double distanceOf (double x, + double y); + + /** + * Return -b/a, from a*x + b*y +c + * + * @return the x/y coefficient + */ + double getInvertedSlope (); + + /** + * Return the mean quadratic distance of the defining population + * of points to the resulting line. + * This can be used to measure how well the line fits the points. + * + * @return the absolute value of the mean distance + */ + double getMeanDistance (); + + /** + * Return the cardinality of the population of defining points. + * + * @return the number of defining points so far + */ + int getNumberOfPoints (); + + /** + * Return -a/b, from a*x + b*y +c. + * + * @return the y/x coefficient + */ + double getSlope (); + + /** + * Add the whole population of another line, which results in + * merging this other line with the line at hand. + * + * @param other the other line + * @return this augmented line, which permits to chain the additions. + */ + Line includeLine (Line other); + + /** + * Add the coordinates of a point in the population of points. + * + * @param x abscissa of the new point + * @param y ordinate of the new point + */ + void includePoint (double x, + double y); + + /** + * Check if line is horizontal ('a' coeff is null) + * + * @return true if horizontal + */ + boolean isHorizontal (); + + /** + * Check if line is vertical ('b' coeff is null). + * + * @return true if vertical + */ + boolean isVertical (); + + /** + * Remove the whole population of points. + * The line is not immediately usable, it needs now to include defining + * points. + */ + void reset (); + + /** + * Return a new line whose coordinates are swapped with respect + * to this one. + * + * @return a new X/Y swapped line + */ + Line swappedCoordinates (); + + /** + * Retrieve the abscissa where the line crosses the given ordinate y. + * Beware of horizontal lines !!! + * + * @param y the imposed ordinate + * @return the corresponding x value + */ + double xAtY (double y); + + /** + * Retrieve the abscissa where the line crosses the given ordinate y, + * rounded to the nearest integer value. + * Beware of horizontal lines !!! + * + * @param y the imposed ordinate + * @return the corresponding x value + */ + int xAtY (int y); + + /** + * Retrieve the ordinate where the line crosses the given abscissa x. + * Beware of vertical lines !!! + * + * @param x the imposed abscissa + * @return the corresponding y value + */ + double yAtX (double x); + + /** + * Retrieve the ordinate where the line crosses the given abscissa x, + * rounded to the nearest integer value. + * Beware of vertical lines !!! + * + * @param x the imposed abscissa + * @return the corresponding y value + */ + int yAtX (int x); + + //~ Inner Classes ---------------------------------------------------------- + /** + * Specific exception raised when trying to invert a + * non-invertible line. + */ + static class NonInvertibleLineException + extends RuntimeException + { + //~ Constructors ------------------------------------------------------- + + NonInvertibleLineException (String message) + { + super(message); + } + } + + /** + * Specific exception raised when trying to use a line with + * undefined parameters. + */ + static class UndefinedLineException + extends RuntimeException + { + //~ Constructors ------------------------------------------------------- + + UndefinedLineException (String message) + { + super(message); + } + } +} diff --git a/src/main/omr/math/LineUtil.java b/src/main/omr/math/LineUtil.java new file mode 100644 index 0000000..91b73b5 --- /dev/null +++ b/src/main/omr/math/LineUtil.java @@ -0,0 +1,98 @@ +//----------------------------------------------------------------------------// +// // +// L i n e U t i l // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.math; + +import java.awt.geom.Line2D; +import java.awt.geom.Point2D; + +/** + * Class {@code LineUtil} is a collection of utilities related to + * lines. + * + * @author Hervé Bitteur + */ +public class LineUtil +{ + //~ Constructors ----------------------------------------------------------- + + private LineUtil () + { + } + + //~ Methods ---------------------------------------------------------------- + //----------// + // bisector // + //----------// + /** + * Return the bisector (french: médiatrice) of the provided segment + * + * @param segment the provided segment + * @return (a segment on) the bisector + */ + public static Line2D bisector (Line2D segment) + { + double x1 = segment.getX1(); + double y1 = segment.getY1(); + + double hdx = (segment.getX2() - x1) / 2; + double hdy = (segment.getY2() - y1) / 2; + + // Use middle as reference point + double mx = x1 + hdx; + double my = y1 + hdy; + + double x3 = mx + hdy; + double y3 = my - hdx; + double x4 = mx - hdy; + double y4 = my + hdx; + + return new Line2D.Double(x3, y3, x4, y4); + } + + //--------------// + // intersection // + //--------------// + /** + * Return the intersection point between infinite line A defined by + * points p1 & p2 and infinite line B defined by points p3 & p4. + * + * @param p1 first point of line A + * @param p2 second point of line A + * @param p3 first point of line B + * @param p4 second point of line B + * @return the intersection point + */ + public static Point2D.Double intersection (Point2D p1, + Point2D p2, + Point2D p3, + Point2D p4) + { + double x1 = p1.getX(); + double y1 = p1.getY(); + double x2 = p2.getX(); + double y2 = p2.getY(); + double x3 = p3.getX(); + double y3 = p3.getY(); + double x4 = p4.getX(); + double y4 = p4.getY(); + + double den = ((x1 - x2) * (y3 - y4)) - ((y1 - y2) * (x3 - x4)); + + double v12 = (x1 * y2) - (y1 * x2); + double v34 = (x3 * y4) - (y3 * x4); + + double x = ((v12 * (x3 - x4)) - ((x1 - x2) * v34)) / den; + double y = ((v12 * (y3 - y4)) - ((y1 - y2) * v34)) / den; + + return new Point2D.Double(x, y); + } +} diff --git a/src/main/omr/math/LinearEvaluator.java b/src/main/omr/math/LinearEvaluator.java new file mode 100644 index 0000000..5c12aad --- /dev/null +++ b/src/main/omr/math/LinearEvaluator.java @@ -0,0 +1,1122 @@ +//----------------------------------------------------------------------------// +// // +// L i n e a r E v a l u a t o r // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.math; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.InputStream; +import java.io.OutputStream; +import java.util.Arrays; +import java.util.Collection; +import java.util.Map; +import java.util.SortedMap; +import java.util.TreeMap; + +import javax.xml.bind.JAXBContext; +import javax.xml.bind.JAXBException; +import javax.xml.bind.Marshaller; +import javax.xml.bind.Unmarshaller; +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlAttribute; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlElementWrapper; +import javax.xml.bind.annotation.XmlID; +import javax.xml.bind.annotation.XmlIDREF; +import javax.xml.bind.annotation.XmlRootElement; +import javax.xml.bind.annotation.adapters.XmlAdapter; +import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter; + +/** + * Class {@code LinearEvaluator} is an evaluator using linear regression. + * + *

It provides a distance between 2 "patterns". A pattern is a vector of + * parameter values in the input domain. + * + *

It provides a distance between a "pattern" from the input domain to a + * "category" in the output range, thus allowing to map patterns to categories. + * This feature can be used for example to map a given Glyph (through the + * pattern of its measured moments values) to the best fitting Shape category. + * + *

This evaluator can be trained, by feeding it with sample patterns for each + * defined category. + * + *

The evaluator data can be marshalled to and unmarshalled from an XML + * formatted stream. + * + * @author Hervé Bitteur + */ +@XmlAccessorType(XmlAccessType.NONE) +@XmlRootElement(name = "linear-evaluator") +public class LinearEvaluator +{ + //~ Static fields/initializers --------------------------------------------- + + /** Usual logger utility */ + private static final Logger logger = LoggerFactory.getLogger( + LinearEvaluator.class); + + /** Un/marshalling context for use with JAXB */ + private static volatile JAXBContext jaxbContext; + + /** To avoid infinity */ + public static final double INFINITE_DISTANCE = 50e50; + + /** To detect a near-zero value in a double */ + private static final double EPSILON = 1E-10; + + //~ Instance fields -------------------------------------------------------- + /** A descriptor for each input parameter. */ + @XmlElementWrapper(name = "defaults") + @XmlElement(name = "parameter") + private final Parameter[] parameters; + + /** A descriptor for each output category. */ + @XmlJavaTypeAdapter(CategoryMapAdapter.class) + @XmlElement(name = "categories") + private final SortedMap categories; + + /** + * Flag to indicate that some data has changed since unmarshalling + * and that engine internals must be marshalled to disk before + * exiting. + */ + private boolean dataModified = false; + + //~ Constructors ----------------------------------------------------------- + //-----------------// + // LinearEvaluator // + //-----------------// + /** + * Creates a new LinearEvaluator object. + * + * @param inputNames the parameter names + */ + public LinearEvaluator (String[] inputNames) + { + categories = new TreeMap<>(); + parameters = new Parameter[inputNames.length]; + + for (int i = 0; i < inputNames.length; i++) { + parameters[i] = new Parameter(inputNames[i]); + } + } + + //-----------------// + // LinearEvaluator // + //-----------------// + /** Private no-arg constructor meant for the JAXB compiler only */ + private LinearEvaluator () + { + categories = null; + parameters = null; + } + + //~ Methods ---------------------------------------------------------------- + // + //-------------------// + // getParameterNames // + //-------------------// + /** + * Report the sequence of parameter names. + * + * @return the sequence of parameter names + */ + public String[] getParameterNames () + { + if (parameters == null) { + return new String[0]; + } else { + String[] names = new String[parameters.length]; + + for (int i = 0; i < parameters.length; i++) { + names[i] = parameters[i].name; + } + + return names; + } + } + + //------------------// + // getCategoryNames // + //------------------// + /** + * Report the collection of category names (order is irrelevant). + * + * @return the collection of category names + */ + public String[] getCategoryNames () + { + if (categories == null) { + return new String[0]; + } else { + Collection values = categories.values(); + String[] names = new String[values.size()]; + + int index = 0; + for (Category cat : values) { + names[index++] = cat.getId(); + } + + return names; + } + } + + //--------------// + // getInputSize // + //--------------// + /** + * Report the number of parameters in the input patterns. + * + * @return the count of pattern parameters + */ + public final int getInputSize () + { + return parameters.length; + } + + //------------------// + // categoryDistance // + //------------------// + /** + * Measure the "distance" information between a given pattern and + * (the mean pattern of) a category. + * + * @param pattern the value for each parameter of the pattern to evaluate + * @param categoryId the category id to measure distance from + * @return the measured distance + */ + public double categoryDistance (double[] pattern, + String categoryId) + { + return checkArguments(pattern, categoryId).distance(pattern, parameters); + } + + //------// + // dump // + //------// + public void dump () + { + System.out.println(); + System.out.println("LinearEvaluator"); + System.out.println("==============="); + System.out.println(); + + // Input size + System.out.println("Inputs : " + getInputSize() + " parameters"); + + // Output size + System.out.println( + "Outputs : " + categories.keySet().size() + " categories"); + + // Description of each category + for (Category category : categories.values()) { + category.dump(); + } + } + + //--------------// + // dumpDistance // + //--------------// + /** + * Print out the "distance" information between a given pattern and + * a category. + * It's a sort of debug information. + * + * @param pattern the pattern at hand + * @param category the category to measure distance from + */ + public void dumpDistance (double[] pattern, + String category) + { + categories.get(category).dumpDistance(pattern, parameters); + } + + //------------// + // getMaximum // + //------------// + /** + * Get the constraint test on maximum for a parameter of the + * provided category. + * + * @param paramIndex the impacted parameter + * @param categoryId the targeted category + * @return the current maximum value (null if test is disabled) + */ + public Double getMaximum (int paramIndex, + String categoryId) + { + return getCategoryParam(paramIndex, categoryId).max; + } + + //------------// + // getMinimum // + //------------// + /** + * Get the constraint test on minimum for a parameter of the + * provided category. + * + * @param paramIndex the impacted parameter + * @param categoryId the targeted category + * @return the current minimum value (null if test is disabled) + */ + public Double getMinimum (int paramIndex, + String categoryId) + { + return getCategoryParam(paramIndex, categoryId).min; + } + + //---------------// + // includeSample // + //---------------// + /** + * Include a new sample (on top of unmarshalled data). + * We use this to widen the min/max constraints, and also to increase + * the population and thus the categories training status. + * + * @param params the parameters + * @param categoryId the targeted category + * @return true if some min/max bound has changed + */ + public boolean includeSample (double[] params, + String categoryId) + { + // Check category label + Category category = categories.get(categoryId); + + if (category == null) { + throw new IllegalArgumentException( + "Unknown category: " + categoryId); + } + + boolean extended = category.include(params); + + // Update categories parameters accordingly + computeCategoriesParams(); + + dataModified = true; + + return extended; + } + + //----------------// + // isDataModified // + //----------------// + /** + * @return true if some data has been modified since unmarshalling + */ + public boolean isDataModified () + { + return dataModified; + } + + //---------// + // marshal // + //---------// + /** + * Marshal the LinearEvaluator to its XML file. + * + * @param os the XML output stream, which is not closed by this method + * @exception JAXBException raised when marshalling goes wrong + */ + public void marshal (OutputStream os) + throws JAXBException + { + Marshaller m = getJaxbContext().createMarshaller(); + m.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true); + m.marshal(this, os); + logger.debug("LinearEvaluator marshalled"); + } + + //-----------------// + // patternDistance // + //-----------------// + /** + * Measure the "distance" information between two patterns. + * + * @param one the first pattern + * @param two the second pattern + * @return the measured distance between them + */ + public double patternDistance (double[] one, + double[] two) + { + final int inputSize = getInputSize(); + + // Check sizes + if ((one == null) + || (one.length != inputSize) + || (two == null) + || (two.length != inputSize)) { + throw new IllegalArgumentException( + "Patterns are null or inconsistent with the LinearEvaluator"); + } + + double dist = 0; + + for (int p = 0; p < inputSize; p++) { + double dif = one[p] - two[p]; + dist += (dif * dif * parameters[p].defaultWeight); + } + + return dist / inputSize; + } + + //-------// + // train // + //-------// + /** + * Perform the training of the evaluator. + * + * @param samples a collection of samples (category + pattern) + */ + public void train (Collection samples) + { + // Check size consistencies. + if ((samples == null) || samples.isEmpty()) { + throw new IllegalArgumentException( + "samples collection is null or empty"); + } + + // Reset counters for each category, if needed + for (Category category : categories.values()) { + for (CategoryParam param : category.params) { + param.reset(); + } + } + + // Accumulate data from samples into categories descriptors + for (Sample sample : samples) { + Category category = categories.get(sample.category); + + if (category == null) { + category = new Category(sample.category, parameters); + categories.put(sample.category, category); + } + + category.include(sample.pattern); + logger.debug("Accu {} count:{}", + category.getId(), category.getCardinality()); + } + + computeCategoriesParams(); + } + + //-----------// + // unmarshal // + //-----------// + /** + * Unmarshal the provided XML stream to allocate the corresponding + * LinearEvaluator. + * + * @param in the input stream that contains the evaluator definition in XML + * format. The stream is not closed by this method + * @return the allocated network. + * @exception JAXBException raised when unmarshalling goes wrong + */ + public static LinearEvaluator unmarshal (InputStream in) + throws JAXBException + { + Unmarshaller um = getJaxbContext().createUnmarshaller(); + LinearEvaluator evaluator = (LinearEvaluator) um.unmarshal(in); + logger.debug("LinearEvaluator unmarshalled"); + + return evaluator; + } + + //----------------// + // getJaxbContext // + //----------------// + private static JAXBContext getJaxbContext () + throws JAXBException + { + // Lazy creation + if (jaxbContext == null) { + jaxbContext = JAXBContext.newInstance(LinearEvaluator.class); + } + + return jaxbContext; + } + + //----------------// + // checkArguments // + //----------------// + private Category checkArguments (double[] pattern, + String categoryId) + { + // Check sizes + if ((pattern == null) || (pattern.length != getInputSize())) { + throw new IllegalArgumentException( + "Pattern is null or inconsistent with the LinearEvaluator"); + } + + // Check category label + Category category = categories.get(categoryId); + + if (category == null) { + throw new IllegalArgumentException( + "Unknown category: " + categoryId); + } + + return category; + } + + //-------------------------// + // computeCategoriesParams // + //-------------------------// + private void computeCategoriesParams () + { + // Compute parameters means & weights for each category + for (Category category : categories.values()) { + logger.debug("Computing {} count:{}", + category.getId(), category.getCardinality()); + category.compute(); + } + + // Compute default weight for each parameter + // (using the sample populations of all categories) + for (int p = 0; p < parameters.length; p++) { + Population paramPop = new Population(); + + for (Category category : categories.values()) { + CategoryParam param = category.params[p]; + + if (param.training != CategoryParam.TrainingStatus.NONE) { + paramPop.includePopulation(param.population); + } + } + + if (paramPop.getCardinality() > 1) { + double var = paramPop.getVariance(); + + if (var >= EPSILON) { + parameters[p].defaultWeight = 1 / var; + } + } + } + } + + //------------------// + // getCategoryParam // + //------------------// + private CategoryParam getCategoryParam (int paramIndex, + String categoryId) + { + // Check category label + Category category = categories.get(categoryId); + + if (category == null) { + throw new IllegalArgumentException( + "Unknown category: " + categoryId); + } + + return category.params[paramIndex]; + } + + //~ Inner Classes ---------------------------------------------------------- + //--------// + // Sample // + //--------// + /** + * Meant to host one sample for training, representing pattern + * values for a given category. + */ + public static class Sample + { + //~ Instance fields ---------------------------------------------------- + + /** The known category */ + public final String category; + + /** The observed pattern */ + public final double[] pattern; + + //~ Constructors ------------------------------------------------------- + public Sample (String category, + double[] pattern) + { + this.category = category; + this.pattern = pattern; + } + + //~ Methods ------------------------------------------------------------ + @Override + public String toString () + { + StringBuilder sb = new StringBuilder("{"); + sb.append(getClass().getSimpleName()); + sb.append(" ").append(category); + sb.append(" ").append(Arrays.toString(pattern)); + sb.append("}"); + + return sb.toString(); + } + } + + //---------// + // Printer // + //---------// + /** + * Printouts meant for analysis of behavior of LinearEvaluator. + */ + public class Printer + { + //~ Instance fields ---------------------------------------------------- + + // Format strings + private final String sf; // For String + + private final String df; // For double + + //~ Constructors ------------------------------------------------------- + public Printer (int width) + { + sf = "%" + width + "s"; + df = "%" + width + "f"; + } + + //~ Methods ------------------------------------------------------------ + public String getDashes () + { + StringBuilder sb = new StringBuilder(); + + for (int p = 0; p < parameters.length; p++) { + sb.append(String.format(sf, "----------")); + } + + return sb.toString(); + } + + public String getDefaults () + { + StringBuilder sb = new StringBuilder(); + + for (Parameter param : parameters) { + sb.append(String.format(df, param.defaultWeight)); + } + + return sb.toString(); + } + + public String getDeltas (double[] one, + double[] two) + { + StringBuilder sb = new StringBuilder(); + + for (int p = 0; p < parameters.length; p++) { + double dif = one[p] - two[p]; + sb.append(String.format(df, dif * dif)); + } + + return sb.toString(); + } + + public String getNames () + { + StringBuilder sb = new StringBuilder(); + + for (Parameter param : parameters) { + sb.append(String.format(sf, param.name)); + } + + return sb.toString(); + } + + public String getWeightedDeltas (double[] one, + double[] two) + { + StringBuilder sb = new StringBuilder(); + + for (int p = 0; p < parameters.length; p++) { + double dif = one[p] - two[p]; + sb.append( + String.format(df, + dif * dif * parameters[p].defaultWeight)); + } + + return sb.toString(); + } + } + + //----------// + // Category // + //----------// + /** + * Meant to encapsulate the regression data for one category. + */ + private static class Category + { + //~ Instance fields ---------------------------------------------------- + + /** Category id */ + @XmlAttribute(name = "id") + private final String id; + + /** A specific descriptor for each parameter */ + @XmlElement(name = "parameter") + final CategoryParam[] params; + + //~ Constructors ------------------------------------------------------- + /** + * Creates a new Category object. + * + * @param id the category id + * @param parameters the sequence of parameter descriptors + */ + public Category (String id, + Parameter[] parameters) + { + this.id = id; + params = new CategoryParam[parameters.length]; + + for (int p = 0; p < params.length; p++) { + params[p] = new CategoryParam(parameters[p]); + } + } + + /** + * Meant to please JAXB + */ + private Category () + { + id = null; + params = null; + } + + //~ Methods ------------------------------------------------------------ + public void compute () + { + if (getCardinality() > 0) { + for (CategoryParam param : params) { + try { + param.compute(); + } catch (Exception ex) { + logger.warn( + "Category {} cannot compute parameters ex:{}", + id, ex); + } + } + } else { + logger.warn("Category {} has no sample", id); + } + } + + public synchronized double distance (double[] pattern, + Parameter[] parameters) + { + double dist = 0; + + for (int p = 0; p < params.length; p++) { + dist += params[p].weightedDelta( + pattern[p], + parameters[p].defaultWeight); + } + + dist /= params.length; + + return dist; + } + + public synchronized void dump () + { + System.out.println( + "\ncategory:" + id + " cardinality:" + getCardinality()); + + for (CategoryParam param : params) { + param.dump(); + } + } + + public synchronized double dumpDistance (double[] pattern, + Parameter[] parameters) + { + if ((pattern == null) || (pattern.length != params.length)) { + throw new IllegalArgumentException( + "dumpDistance." + + " Pattern array is null or non compatible in length "); + } + + if (getCardinality() >= 2) { + double dist = 0; + + for (int p = 0; p < params.length; p++) { + CategoryParam param = params[p]; + double wDelta = param.weightedDelta( + pattern[p], + parameters[p].defaultWeight); + dist += wDelta; + System.out.printf( + "%2d-> weight:%e wDelta:%e\n", + p, + param.weight, + wDelta); + } + + dist /= params.length; + System.out.println("Dist to cat " + id + " = " + dist); + + return dist; + } else { + return INFINITE_DISTANCE; + } + } + + public int getCardinality () + { + return params[0].population.getCardinality(); + } + + /** + * @return the id + */ + public String getId () + { + return id; + } + + /** Include data from the provided pattern into category descriptor */ + public synchronized boolean include (double[] pattern) + { + boolean extended = false; + + if ((pattern == null) || (pattern.length != params.length)) { + throw new IllegalArgumentException( + "include." + + " Pattern array is null or non compatible in length "); + } + + for (int p = 0; p < params.length; p++) { + if (params[p].includeValue(pattern[p])) { + extended = true; + } + } + + return extended; + } + } + + //--------------------// + // CategoryMapAdapter // + //--------------------// + /** + * Meant for JAXB support of a map. + */ + private static class CategoryMapAdapter + extends XmlAdapter> + { + //~ Constructors ------------------------------------------------------- + + /** + * Meant to please JAXB + */ + public CategoryMapAdapter () + { + } + + //~ Methods ------------------------------------------------------------ + //-----------// + // unmarshal // + //-----------// + @Override + public Category[] marshal (Map map) + throws Exception + { + return map.values().toArray(new Category[map.size()]); + } + + //-----------// + // unmarshal // + //-----------// + @Override + public Map unmarshal (Category[] categories) + { + SortedMap map = new TreeMap<>(); + + for (Category category : categories) { + map.put(category.getId(), category); + } + + return map; + } + } + + //---------------// + // CategoryParam // + //---------------// + /** + * Meant to encapsulate the regression data for one parameter in + * the context of a category. + */ + private static class CategoryParam + { + //~ Static fields/initializers ----------------------------------------- + + /** Used instead of infinitive weight, when variance is zero */ + private static final double HIGH_WEIGHT_FACTOR = 10; + + //~ Enumerations ------------------------------------------------------- + /** Description of the training done so far on a parameter */ + public static enum TrainingStatus + { + //~ Enumeration constant initializers ------------------------------ + + /** + * Not trained + * => no mean value, no weight + */ + NONE, + /** + * Just one data element + * => a mean value, but artificial (average) weight + */ + SINGLE_DATA, + /** + * Several data elements, but with identical values + * => a mean value, but infinite weight + */ + IDENTICAL_VALUES, + /** + * Several data elements, with some variation in the values + * => a mean value and weight computed as 1/variance + */ + NOMINAL; + + } + + //~ Instance fields ---------------------------------------------------- + /** Population to compute mean value & std deviation */ + @XmlElement(name = "population") + private Population population; + + /** Maximum value for this parameter */ + @XmlAttribute(name = "max") + private Double max = null; + + /** Mean value for this parameter */ + @XmlAttribute(name = "mean") + private double mean; + + /** Minimum value for this parameter */ + @XmlAttribute(name = "min") + private Double min = null; + + /** Weight for this parameter */ + @XmlAttribute(name = "weight") + private double weight; + + /** Training status */ + @XmlAttribute(name = "training") + private TrainingStatus training = TrainingStatus.NONE; + + /** Related parameter descriptor */ + @XmlIDREF + @XmlAttribute(name = "name") + private Parameter parameter; + + //~ Constructors ------------------------------------------------------- + public CategoryParam (Parameter parameter) + { + this.parameter = parameter; + population = new Population(); + } + + /** + * Meant to please JAXB + */ + public CategoryParam () + { + } + + //~ Methods ------------------------------------------------------------ + /** Compute the param characteristics out of its data sample */ + public void compute () + { + int count = population.getCardinality(); + + if (count > 0) { + mean = population.getMeanValue(); + } + + if (count == 1) { + training = TrainingStatus.SINGLE_DATA; + } else { + double var = population.getVariance(); + + if (var < EPSILON) { + training = TrainingStatus.IDENTICAL_VALUES; + } else { + training = TrainingStatus.NOMINAL; + weight = 1 / var; + } + } + } + + public void dump () + { + StringBuilder sb = new StringBuilder(); + + sb.append(" ").append(parameter); + + sb.append(" training=").append(training); + + sb.append(" min=").append(min); + + sb.append(" mean=").append(mean); + + sb.append(" max=").append(max); + + sb.append(" weight=").append(weight); + + if (population.getCardinality() > 1) { + sb.append(" var=").append(population.getVariance()); + } + + System.out.println(sb); + } + + /** + * Include a new value for this category parameter. + * + * @param val the new value + * @return true if any of the min/max bounds has changed + */ + public boolean includeValue (double val) + { + boolean extended = false; + + // Cumulate into Population + population.includeValue(val); + + // Handle min value + if (min != null) { + if (val < min) { + min = val; + extended = true; + } + } else { + min = val; + extended = true; + } + + // Handle max value + if (max != null) { + if (val > max) { + max = val; + extended = true; + } + } else { + max = val; + extended = true; + } + + return extended; + } + + public void reset () + { + population.reset(); + min = null; + max = null; + mean = 0; + weight = 0; + } + + /** + * Report the weighted square delta of a value vs param mean value + * + * @param val the observed value + * @param stdWeight the standard average weight + * @return the weighted square delta + */ + public double weightedDelta (double val, + double stdWeight) + { + if (training == TrainingStatus.NONE) { + return INFINITE_DISTANCE; + } else { + double dif = mean - val; + + return dif * dif * getWeight(stdWeight); + } + } + + /** + * Report the proper value to be used for parameter weight. + * + * @param stdWeight the standard average weight + * @return the proper weight value + */ + private double getWeight (double stdWeight) + { + switch (training) { + case NONE: + return 0; + + case SINGLE_DATA: + return stdWeight; + + case IDENTICAL_VALUES: + return stdWeight * HIGH_WEIGHT_FACTOR; + + default: + return weight; + } + } + } + + //-----------// + // Parameter // + //-----------// + /** + * Description of an input parameter for the LinearEvaluator. + */ + private static class Parameter + { + //~ Instance fields ---------------------------------------------------- + + /** Default weight */ + @XmlAttribute(name = "weight") + public double defaultWeight; + + /** Name used for this parameter */ + @XmlID + @XmlAttribute(name = "name") + public final String name; + + //~ Constructors ------------------------------------------------------- + /** + * Creates a new Parameter object. + * + * @param name the unique name for this parameter + */ + public Parameter (String name) + { + this.name = name; + } + + /** + * Needed by JAXB + */ + public Parameter () + { + name = null; + } + + //~ Methods ------------------------------------------------------------ + @Override + public String toString () + { + return "{Param " + name + " defaultWeight:" + defaultWeight + "}"; + } + } +} diff --git a/src/main/omr/math/NaturalSpline.java b/src/main/omr/math/NaturalSpline.java new file mode 100644 index 0000000..0e14dca --- /dev/null +++ b/src/main/omr/math/NaturalSpline.java @@ -0,0 +1,427 @@ +//----------------------------------------------------------------------------// +// // +// N a t u r a l S p l i n e // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.math; + +import java.awt.Shape; +import java.awt.geom.CubicCurve2D; +import java.awt.geom.Line2D; +import static java.awt.geom.PathIterator.*; +import java.awt.geom.Point2D; +import java.awt.geom.QuadCurve2D; + +/** + * Class {@code NaturalSpline} defines a natural (cubic) spline + * interpolated on a sequence of knots. + * + *

Internally the spline is composed of a sequence of curves, one + * curve between two consecutive knots. + * Each curve is a bezier curve defined by the 2 related knots separated by 2 + * control points.

+ * + *

At each knot, continuity in ensured up to the second derivative. + * The second derivative is set to zero at first and last knots of the whole + * spline.

+ * + *

Degenerated cases: When the sequence of knots contains only 3 or 2 points, + * the spline degenerates to a quadratic or a straight line respectively. + * If less than two points are provided, the spline cannot be created.

+ * + *

Cf + * http://www.cse.unsw.edu.au/~lambert/splines/

+ * + * @author Hervé Bitteur + */ +public class NaturalSpline + extends GeoPath + implements Line +{ + //~ Constructors ----------------------------------------------------------- + + //---------------// + // NaturalSpline // + //---------------// + /** + * Creates a new NaturalSpline object from a sequence of connected shapes + * + * @param curves the smooth sequence of shapes (cubic curves expected) + */ + private NaturalSpline (Shape... curves) + { + for (Shape shape : curves) { + append(shape, true); + } + } + + //~ Methods ---------------------------------------------------------------- + //-------------// + // interpolate // + //-------------// + /** + * Create the natural spline that interpolates the provided knots + * + * @param points the provided points + * @return the resulting spline curve + */ + public static NaturalSpline interpolate (Point2D... points) + { + // Check parameters + if (points == null) { + throw new IllegalArgumentException( + "NaturalSpline cannot interpolate null arrays"); + } + + double[] xx = new double[points.length]; + double[] yy = new double[points.length]; + + for (int i = 0; i < points.length; i++) { + Point2D pt = points[i]; + xx[i] = pt.getX(); + yy[i] = pt.getY(); + } + + return interpolate(xx, yy); + } + + //-------------// + // interpolate // + //-------------// + /** + * Create the natural spline that interpolates the provided knots + * + * @param xx the abscissae of the provided points + * @param yy the ordinates of the provided points + * @return the resulting spline curve + */ + public static NaturalSpline interpolate (double[] xx, + double[] yy) + { + // Check parameters + if ((xx == null) || (yy == null)) { + throw new IllegalArgumentException( + "NaturalSpline cannot interpolate null arrays"); + } + + if (xx.length != yy.length) { + throw new IllegalArgumentException( + "NaturalSpline interpolation needs consistent coordinates"); + } + + // Number of segments + final int n = xx.length - 1; + + if (n < 1) { + throw new IllegalArgumentException( + "NaturalSpline interpolation needs at least 2 points"); + } + + if (n == 1) { + // Use a Line + return new NaturalSpline( + new Line2D.Double(xx[0], yy[0], xx[1], yy[1])); + } else if (n == 2) { + // Use a Quadratic (TODO: check this formula...) + // double t = (xx[1] - xx[0]) / (xx[2] - xx[0]); + // double u = 1 - t; + // double cpx = (xx[1] - (u * u * xx[0]) - (t * t * xx[2])) / 2 * t * u; + // double cpy = (yy[1] - (u * u * yy[0]) - (t * t * yy[2])) / 2 * t * u; + return new NaturalSpline( + new QuadCurve2D.Double( + xx[0], + yy[0], + (2 * xx[1]) - ((xx[0] + xx[2]) / 2), + (2 * yy[1]) - ((yy[0] + yy[2]) / 2), + xx[2], + yy[2])); + } else { + // Use a sequence of cubics + double[] dx = getCubicDerivatives(xx); + double[] dy = getCubicDerivatives(yy); + Shape[] curves = new Shape[n]; + + for (int i = 0; i < n; i++) { + // Build each segment curve + curves[i] = new CubicCurve2D.Double( + xx[i], + yy[i], + xx[i] + (dx[i] / 3), + yy[i] + (dy[i] / 3), + xx[i + 1] - (dx[i + 1] / 3), + yy[i + 1] - (dy[i + 1] / 3), + xx[i + 1], + yy[i + 1]); + } + + return new NaturalSpline(curves); + } + } + + @Override + public double distanceOf (double x, + double y) + { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public double getInvertedSlope () + { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public double getMeanDistance () + { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public int getNumberOfPoints () + { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public double getSlope () + { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public Line includeLine (Line other) + { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public void includePoint (double x, + double y) + { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public boolean isHorizontal () + { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public boolean isVertical () + { + throw new UnsupportedOperationException("Not supported yet."); + } + + // //------------// + // // renderLine // + // //------------// + // /** + // * Specific rendering of the curved line + // * @param g graphical context + // * @param r radius for control and defining points + // */ + // public void renderLine (Graphics2D g, + // double r) + // { + // // Draw the curved line itself + // g.draw(this); + // + // // Draw the control & defining points on top of it + // Color oldColor = g.getColor(); + // Ellipse2D ellipse = new Ellipse2D.Double(); + // double[] buffer = new double[6]; + // + // for (PathIterator it = getPathIterator(null); !it.isDone(); + // it.next()) { + // int segmentKind = it.currentSegment(buffer); + // int coords = countOf(segmentKind); + // boolean control = false; + // + // for (int ic = coords - 2; ic >= 0; ic -= 2) { + // ellipse.setFrame( + // buffer[ic] - r, + // buffer[ic + 1] - r, + // 2 * r, + // 2 * r); + // g.setColor(control ? Color.PINK : Color.BLUE); + // g.fill(ellipse); + // + // control = true; + // } + // } + // + // g.setColor(oldColor); + // } + @Override + public Line swappedCoordinates () + { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public int xAtY (int y) + { + return (int) Math.rint(xAtY((double) y)); + } + + //----------------// + // xDerivativeAtY // + //----------------// + /** + * Report the abscissa derivative value of the spline at provided ordinate + * (assuming true function) + * + * @param y the provided ordinate + * @return the x derivative value at this ordinate + */ + public double xDerivativeAtY (double y) + { + final double[] buffer = new double[6]; + final Point2D.Double p1 = new Point2D.Double(); + final Point2D.Double p2 = new Point2D.Double(); + final int segmentKind = getYSegment(y, buffer, p1, p2); + final double deltaY = p2.y - p1.y; + final double t = (y - p1.y) / deltaY; + final double u = 1 - t; + + // dx/dy = dx/dt * dt/dy + // dt/dy = 1/deltaY + switch (segmentKind) { + case SEG_LINETO: + return (p2.x - p1.x) / deltaY; + + case SEG_QUADTO: { + double cpx = buffer[0]; + + return ((-2 * p1.x * u) + (2 * cpx * (1 - (2 * t))) + + (2 * p2.x * t)) / deltaY; + } + + case SEG_CUBICTO: { + double cpx1 = buffer[0]; + double cpx2 = buffer[2]; + + return ((-3 * p1.x * u * u) + (3 * cpx1 * ((u * u) - (2 * u * t))) + + (3 * cpx2 * ((2 * t * u) - (t * t))) + (3 * p2.x * t * t)) / deltaY; + } + + default: + throw new RuntimeException("Illegal currentSegment " + segmentKind); + } + } + + @Override + public int yAtX (int x) + { + return (int) Math.rint((double) x); + } + + //----------------// + // yDerivativeAtX // + //----------------// + /** + * Report the ordinate derivative value of the spline at provided abscissa + * (assuming true function) + * + * @param x the provided abscissa + * @return the y derivative value at this abscissa + */ + public double yDerivativeAtX (double x) + { + final double[] buffer = new double[6]; + final Point2D.Double p1 = new Point2D.Double(); + final Point2D.Double p2 = new Point2D.Double(); + final int segmentKind = getXSegment(x, buffer, p1, p2); + final double deltaX = p2.x - p1.x; + final double t = (x - p1.x) / deltaX; + final double u = 1 - t; + + // dy/dx = dy/dt * dt/dx + // dt/dx = 1/deltaX + switch (segmentKind) { + case SEG_LINETO: + return (p2.y - p1.y) / deltaX; + + case SEG_QUADTO: { + double cpy = buffer[1]; + + return ((-2 * p1.y * u) + (2 * cpy * (1 - (2 * t))) + + (2 * p2.y * t)) / deltaX; + } + + case SEG_CUBICTO: { + double cpy1 = buffer[1]; + double cpy2 = buffer[3]; + + return ((-3 * p1.y * u * u) + (3 * cpy1 * ((u * u) - (2 * u * t))) + + (3 * cpy2 * ((2 * t * u) - (t * t))) + (3 * p2.y * t * t)) / deltaX; + } + + default: + throw new RuntimeException("Illegal currentSegment " + segmentKind); + } + } + + //---------------------// + // getCubicDerivatives // + //---------------------// + /** + * Computes the derivatives of natural cubic spline that interpolates the + * provided knots + * + * @param z the provided n knots + * @return the corresponding array of derivative values + */ + private static double[] getCubicDerivatives (double[] z) + { + // Number of segments + final int n = z.length - 1; + + // Compute the derivative at each provided knot + double[] D = new double[n + 1]; + + /* Equation to solve: + * [2 1 ] [D[0]] [3(z[1] - z[0]) ] + * |1 4 1 | |D[1]| |3(z[2] - z[0]) | + * | 1 4 1 | | . | = | . | + * | ..... | | . | | . | + * | 1 4 1| | . | |3(z[n] - z[n-2])| + * [ 1 2] [D[n]] [3(z[n] - z[n-1])] + * by using row operations to convert the matrix to upper triangular + * and then back sustitution. + */ + double[] gamma = new double[n + 1]; + gamma[0] = 1.0f / 2.0f; + + for (int i = 1; i < n; i++) { + gamma[i] = 1 / (4 - gamma[i - 1]); + } + + gamma[n] = 1 / (2 - gamma[n - 1]); + + double[] delta = new double[n + 1]; + delta[0] = 3 * (z[1] - z[0]) * gamma[0]; + + for (int i = 1; i < n; i++) { + delta[i] = ((3 * (z[i + 1] - z[i - 1])) - delta[i - 1]) * gamma[i]; + } + + delta[n] = ((3 * (z[n] - z[n - 1])) - delta[n - 1]) * gamma[n]; + + D[n] = delta[n]; + + for (int i = n - 1; i >= 0; i--) { + D[i] = delta[i] - (gamma[i] * D[i + 1]); + } + + return D; + } +} diff --git a/src/main/omr/math/NeuralNetwork.java b/src/main/omr/math/NeuralNetwork.java new file mode 100644 index 0000000..443ec35 --- /dev/null +++ b/src/main/omr/math/NeuralNetwork.java @@ -0,0 +1,929 @@ +//----------------------------------------------------------------------------// +// // +// N e u r a l N e t w o r k // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.math; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.InputStream; +import java.io.OutputStream; + +import javax.xml.bind.JAXBContext; +import javax.xml.bind.JAXBException; +import javax.xml.bind.Marshaller; +import javax.xml.bind.Unmarshaller; +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlAttribute; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlElementWrapper; +import javax.xml.bind.annotation.XmlRootElement; + +/** + * Class {@code NeuralNetwork} implements a back-propagation neural + * network, with one input layer, one hidden layer and one output layer. + * The transfer function is the sigmoid. + * + *

This neuralNetwork class can be stored on disk in XML form (through + * the {@link #marshal} and {@link #unmarshal} methods). + * + *

The class also allows in-memory {@link #backup} and {@link #restore} + * operation, mainly used to save the most performant weight values during the + * network training. + * + * @author Hervé Bitteur + */ +@XmlAccessorType(XmlAccessType.NONE) +@XmlRootElement(name = "neural-network") +public class NeuralNetwork +{ + //~ Static fields/initializers --------------------------------------------- + + /** Usual logger utility */ + private static final Logger logger = LoggerFactory.getLogger( + NeuralNetwork.class); + + /** Un/marshalling context for use with JAXB */ + private static volatile JAXBContext jaxbContext; + + //~ Instance fields -------------------------------------------------------- + // + /** Size of input layer. */ + @XmlAttribute(name = "input-size") + private final int inputSize; + + /** Size of hidden layer. */ + @XmlAttribute(name = "hidden-size") + private final int hiddenSize; + + /** Size of output layer. */ + @XmlAttribute(name = "output-size") + private final int outputSize; + + /** Labels of input cells. */ + @XmlElementWrapper(name = "input-labels") + @XmlElement(name = "input") + private final String[] inputLabels; + + /** Labels of output cells. */ + @XmlElementWrapper(name = "output-labels") + @XmlElement(name = "output") + private final String[] outputLabels; + + /** Weights to hidden layer. */ + @XmlElementWrapper(name = "hidden-weights") + @XmlElement(name = "row") + private double[][] hiddenWeights; + + /** Weights to output layer. */ + @XmlElementWrapper(name = "output-weights") + @XmlElement(name = "row") + private double[][] outputWeights; + + /** Flag to stop training. */ + private transient volatile boolean stopping = false; + + /** Learning Rate parameter. */ + private transient volatile double learningRate = 0.40; + + /** Max Error parameter. */ + private transient volatile double maxError = 1E-4; + + /** Momentum for faster convergence. */ + private transient volatile double momentum = 0.25; + + /** Number of epochs when training. */ + private transient volatile int epochs = 1000; + + //~ Constructors ----------------------------------------------------------- + //---------------// + // NeuralNetwork // + //---------------// + /** + * Create a neural network, with specified number of cells in each + * layer, and default values. + * + * @param inputSize number of cells in input layer + * @param hiddenSize number of cells in hidden layer + * @param outputSize number of cells in output layer + * @param amplitude amplitude ( <= 1.0) for initial random values + * @param inputLabels array + * o f + * labels + * for + * input + * cells, + * or + * null + * @param outputLabels array of labels for output cells, or null + */ + public NeuralNetwork (int inputSize, + int hiddenSize, + int outputSize, + double amplitude, + String[] inputLabels, + String[] outputLabels) + { + // Cache parameters + this.inputSize = inputSize; + this.hiddenSize = hiddenSize; + this.outputSize = outputSize; + + // Allocate weights (from input) to hidden layer + // +1 for bias + hiddenWeights = createMatrix(hiddenSize, inputSize + 1, amplitude); + + // Allocate weights (from hidden) to output layer + // +1 for bias + outputWeights = createMatrix(outputSize, hiddenSize + 1, amplitude); + + // Labels for input, if any + this.inputLabels = inputLabels; + + if ((inputLabels != null) && (inputLabels.length != inputSize)) { + throw new IllegalArgumentException( + "Inconsistent input labels " + inputLabels + " vs " + + inputSize); + } + + // Labels for output, if any + this.outputLabels = outputLabels; + + if ((outputLabels != null) && (outputLabels.length != outputSize)) { + throw new IllegalArgumentException( + "Inconsistent output labels " + outputLabels + " vs " + + outputSize); + } + + logger.debug("Network created"); + } + + //---------------// + // NeuralNetwork // + //---------------// + /** + * Create a neural network, with specified number of cells in each + * layer, and specific parameters + * + * @param inputSize number of cells in input layer + * @param hiddenSize number of cells in hidden layer + * @param outputSize number of cells in output layer + * @param amplitude amplitude ( <= 1.0) for initial random values + * @param inputLabels array + * o f + * labels + * for + * input + * cells, + * or + * null + * @param outputLabels array of labels for output cells, or null + * @param learningRate learning rate factor + * @param momentum momentum from last adjustment + * @param maxError threshold to stop training + * @param epochs number of epochs in training + */ + public NeuralNetwork (int inputSize, + int hiddenSize, + int outputSize, + double amplitude, + String[] inputLabels, + String[] outputLabels, + double learningRate, + double momentum, + double maxError, + int epochs) + { + this( + inputSize, + hiddenSize, + outputSize, + amplitude, + inputLabels, + outputLabels); + + // Cache parameters + this.learningRate = learningRate; + this.momentum = momentum; + this.maxError = maxError; + this.epochs = epochs; + } + + //---------------// + // NeuralNetwork // + //---------------// + /** Private no-arg constructor meant for the JAXB compiler only */ + private NeuralNetwork () + { + inputSize = -1; + hiddenSize = -1; + outputSize = -1; + inputLabels = null; + outputLabels = null; + } + + //~ Methods ---------------------------------------------------------------- + //-----------// + // unmarshal // + //-----------// + /** + * Unmarshal the provided XML stream to allocate the corresponding + * NeuralNetwork. + * + * @param in the input stream that contains the network definition in XML + * format. The stream is not closed by this method + * + * @return the allocated network. + * @exception JAXBException raised when unmarshalling goes wrong + */ + public static NeuralNetwork unmarshal (InputStream in) + throws JAXBException + { + Unmarshaller um = getJaxbContext() + .createUnmarshaller(); + NeuralNetwork nn = (NeuralNetwork) um.unmarshal(in); + logger.debug("Network unmarshalled"); + + return nn; + } + + // + //--------// + // backup // + //--------// + /** + * Return a backup of the internal memory of this network. + * Generally used right after network creation to save the initial + * conditions. + * + * @return an opaque copy of the network memory + */ + public Backup backup () + { + logger.debug("Network memory backup"); + + return new Backup(hiddenWeights, outputWeights); + } + + //------// + // dump // + //------// + /** + * Dumps the network + */ + public void dump () + { + StringBuilder sb = new StringBuilder(); + sb.append(String.format("Network%n")); + sb.append(String.format("LearningRate = %f%n", learningRate)); + sb.append(String.format("Momentum = %f%n", momentum)); + sb.append(String.format("MaxError = %f%n", maxError)); + sb.append(String.format("Epochs = %d%n", epochs)); + + // Input + sb.append(String.format("%nInputs : %d cells%n", inputSize)); + + // Hidden + sb.append(dumpOfMatrix(hiddenWeights)); + sb.append(String.format("%nHidden : %d cells%n", hiddenSize)); + + // Output + sb.append(dumpOfMatrix(outputWeights)); + sb.append(String.format("%nOutputs : %d cells%n", outputSize)); + + logger.info(sb.toString()); + } + + //---------------// + // getHiddenSize // + //---------------// + /** + * Report the number of cells in the hidden layer + * + * @return the size of the hidden layer + */ + public int getHiddenSize () + { + return hiddenSize; + } + + //----------------// + // getInputLabels // + //----------------// + /** + * Report the input labels, if any. + * + * @return the inputLabels, perhaps null + */ + public String[] getInputLabels () + { + return inputLabels; + } + + //--------------// + // getInputSize // + //--------------// + /** + * Report the number of cells in the input layer + * + * @return the size of input layer + */ + public int getInputSize () + { + return inputSize; + } + + //-----------------// + // getOutputLabels // + //-----------------// + /** + * Report the output labels, if any. + * + * @return the outputLabels, perhaps null + */ + public String[] getOutputLabels () + { + return outputLabels; + } + + //---------------// + // getOutputSize // + //---------------// + /** + * Report the size of the output layer + * + * @return the number of cells in the output layer + */ + public int getOutputSize () + { + return outputSize; + } + + //---------// + // marshal // + //---------// + /** + * Marshal the NeuralNetwork to its XML file + * + * @param os the XML output stream, which is not closed by this method + * @exception JAXBException raised when marshalling goes wrong + */ + public void marshal (OutputStream os) + throws JAXBException + { + Marshaller m = getJaxbContext() + .createMarshaller(); + m.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true); + m.marshal(this, os); + logger.debug("Network marshalled"); + } + + //---------// + // restore // + //---------// + /** + * Restore the internal memory of a Network, from a previous Backup. + * This does not reset the current parameters such as learning rate, + * momentum, maxError or epochs. + * + * @param backup a backup previously made + */ + public void restore (Backup backup) + { + // Check parameter + if (backup == null) { + throw new IllegalArgumentException("Backup is null"); + } + + // Make sure backup is compatible with this neural network + if ((backup.hiddenWeights.length != hiddenSize) + || (backup.hiddenWeights[0].length != (inputSize + 1)) + || (backup.outputWeights.length != outputSize) + || (backup.outputWeights[0].length != (hiddenSize + 1))) { + throw new IllegalArgumentException("Incompatible backup"); + } + + logger.debug("Network memory restore"); + this.hiddenWeights = cloneMatrix(backup.hiddenWeights); + this.outputWeights = cloneMatrix(backup.outputWeights); + } + + //-----// + // run // + //-----// + /** + * Run the neural network on an array of input values, and return the + * computed output values. + * This method writes into the hiddens buffer. + * + * @param inputs the provided input values + * @param hiddens provided buffer for hidden values, or null + * @param outputs preallocated array for the computed output values, or null + * if not already allocated + * + * @return the computed output values + */ + public double[] run (double[] inputs, + double[] hiddens, + double[] outputs) + { + // Check size consistencies. + if (inputs == null) { + logger.error("run method. inputs array is null"); + } else if (inputs.length != inputSize) { + logger.error( + "run method. input size {} not consistent with" + + " network input layer {}", + inputs.length, + inputSize); + } + + // Allocate the hiddens if not provided + if (hiddens == null) { + hiddens = new double[hiddenSize]; + } + + // Compute the hidden values + forward(inputs, hiddenWeights, hiddens); + + // Allocate the outputs if not done yet + if (outputs == null) { + outputs = new double[outputSize]; + } else if (outputs.length != outputSize) { + logger.error( + "run method. output size {} not consistent with" + + " network output layer {}", + outputs.length, + outputSize); + } + + // Then, compute the output values + forward(hiddens, outputWeights, outputs); + + return outputs; + } + + //-----------// + // setEpochs // + //-----------// + /** + * Set the number of iterations for training the network with a + * given input. + * + * @param epochs number of iterations + */ + public void setEpochs (int epochs) + { + this.epochs = epochs; + } + + //-----------------// + // setLearningRate // + //-----------------// + /** + * Set the learning rate. + * + * @param learningRate the learning rate to use for each iteration + * (typically in the 0.0 .. 1.0 range) + */ + public void setLearningRate (double learningRate) + { + this.learningRate = learningRate; + } + + //-------------// + // setMaxError // + //-------------// + /** + * Set the maximum error level. + * + * @param maxError maximum error + */ + public void setMaxError (double maxError) + { + this.maxError = maxError; + } + + //-------------// + // setMomentum // + //-------------// + /** + * Set the momentum value. + * + * @param momentum the fraction of previous move to be reported on the next + * correction + */ + public void setMomentum (double momentum) + { + this.momentum = momentum; + } + + //------// + // stop // + //------// + /** + * A means to externally stop the current training. + */ + public void stop () + { + stopping = true; + logger.debug("Network training being stopped ..."); + } + + //-------// + // train // + //-------// + /** + * Train the neural network on a collection of input patterns, + * so that it delivers the expected outputs within maxError. + * This method is not optimized for absolute speed, but rather for being + * able to keep the best weights values. + * + * @param inputs the provided patterns of values for input cells + * @param desiredOutputs the corresponding desired values for output cells + * @param monitor a monitor interface to be kept informed (or null) + * + * @return mse, the final mean square error + */ + public double train (double[][] inputs, + double[][] desiredOutputs, + Monitor monitor) + { + logger.debug("Network being trained"); + stopping = false; + + long startTime = System.currentTimeMillis(); + + // Check size consistencies. + if (inputs == null) { + throw new IllegalArgumentException("inputs array is null"); + } + + final int patternNb = inputs.length; + + if (desiredOutputs == null) { + throw new IllegalArgumentException("desiredOutputs array is null"); + } + + // Allocate needed arrays + double[] gottenOutputs = new double[outputSize]; + double[] hiddenGrads = new double[hiddenSize]; + double[] outputGrads = new double[outputSize]; + double[][] hiddenDeltas = createMatrix(hiddenSize, inputSize + 1, 0); + double[][] outputDeltas = createMatrix(outputSize, hiddenSize + 1, 0); + double[] hiddens = new double[hiddenSize]; + + // Mean Square Error + double mse = 0; + + // Notify Monitor we are starting + if (monitor != null) { + // Compute the initial mse + for (int ip = 0; ip < patternNb; ip++) { + run(inputs[ip], hiddens, gottenOutputs); + + for (int o = outputSize - 1; o >= 0; o--) { + double out = gottenOutputs[o]; + double dif = desiredOutputs[ip][o] - out; + mse += (dif * dif); + } + } + + mse /= patternNb; + mse = Math.sqrt(mse); + monitor.trainingStarted(0, mse); + } + + int ie = 0; + + for (; ie < epochs; ie++) { + // Have we been told to stop ? + if (stopping) { + logger.debug("Network stopped."); + + break; + } + + // Compute the output layer error terms + mse = 0; + + // Loop on all input patterns + for (int ip = 0; ip < patternNb; ip++) { + // Run the network with input values and current weights + run(inputs[ip], hiddens, gottenOutputs); + + for (int o = outputSize - 1; o >= 0; o--) { + double out = gottenOutputs[o]; + double dif = desiredOutputs[ip][o] - out; + mse += (dif * dif); + outputGrads[o] = dif * out * (1 - out); + } + + // Compute the hidden layer error terms + for (int h = hiddenSize - 1; h >= 0; h--) { + double sum = 0; + double hid = hiddens[h]; + + for (int o = outputSize - 1; o >= 0; o--) { + sum += (outputGrads[o] * outputWeights[o][h + 1]); + } + + hiddenGrads[h] = sum * hid * (1 - hid); + } + + // Now update the output weights + for (int o = outputSize - 1; o >= 0; o--) { + for (int h = hiddenSize - 1; h >= 0; h--) { + double dw = (learningRate * outputGrads[o] * hiddens[h]) + + (momentum * outputDeltas[o][h + 1]); + outputWeights[o][h + 1] += dw; + outputDeltas[o][h + 1] = dw; + } + + // Bias + double dw = (learningRate * outputGrads[o]) + + (momentum * outputDeltas[o][0]); + outputWeights[o][0] += dw; + outputDeltas[o][0] = dw; + } + + // And the hidden weights + for (int h = hiddenSize - 1; h >= 0; h--) { + for (int i = inputSize - 1; i >= 0; i--) { + double dw = (learningRate * hiddenGrads[h] * inputs[ip][i]) + + (momentum * hiddenDeltas[h][i + 1]); + hiddenWeights[h][i + 1] += dw; + hiddenDeltas[h][i + 1] = dw; + } + + // Bias + double dw = (learningRate * hiddenGrads[h]) + + (momentum * hiddenDeltas[h][0]); + hiddenWeights[h][0] += dw; + hiddenDeltas[h][0] = dw; + } + } // for (int ip = 0; i < patternNb; i++) + + // Compute true current mse + mse = 0d; + + for (int ip = 0; ip < patternNb; ip++) { + run(inputs[ip], hiddens, gottenOutputs); + + for (int o = outputSize - 1; o >= 0; o--) { + double out = gottenOutputs[o]; + double dif = desiredOutputs[ip][o] - out; + mse += (dif * dif); + } + } + + mse /= patternNb; + mse = Math.sqrt(mse); + + if (monitor != null) { + monitor.epochEnded(ie, mse); + } + + if (mse <= maxError) { + logger.info( + "Network exiting training, remaining error limit reached"); + logger.info("Network remaining error was : {}", mse); + + break; + } + } // for (int ie = 0; ie < epochs; ie++) + + if (logger.isDebugEnabled()) { + long stopTime = System.currentTimeMillis(); + logger.debug( + String.format( + "Duration %,d seconds, %d epochs on %d patterns", + (stopTime - startTime) / 1000, + ie, + patternNb)); + } + + return mse; + } + + //-------------// + // cloneMatrix // + //-------------// + /** + * Create a clone of the provided matrix. + * + * @param matrix the matrix to clone + * @return the clone + */ + private static double[][] cloneMatrix (double[][] matrix) + { + final int rowNb = matrix.length; + final int colNb = matrix[0].length; + + double[][] clone = new double[rowNb][]; + + for (int row = rowNb - 1; row >= 0; row--) { + clone[row] = new double[colNb]; + System.arraycopy(matrix[row], 0, clone[row], 0, colNb); + } + + return clone; + } + + //--------------// + // createMatrix // + //--------------// + /** + * Create and initialize a matrix, with random values. + * Random values are between -amplitude and +amplitude + * + * @param rowNb number of rows + * @param colNb number of columns + * + * @return the properly initialized matrix + */ + private static double[][] createMatrix (int rowNb, + int colNb, + double amplitude) + { + double[][] matrix = new double[rowNb][]; + + for (int row = rowNb - 1; row >= 0; row--) { + double[] vector = new double[colNb]; + matrix[row] = vector; + + for (int col = colNb - 1; col >= 0; col--) { + vector[col] = amplitude * (1.0 - (2 * Math.random())); + } + } + + return matrix; + } + + //------------// + // dumpMatrix // + //------------// + /** + * Dump a matrix (assumed to be a true rectangular matrix, + * with all rows of the same length). + * + * @param matrix the matrix to dump + * @return the matrix representation + */ + private String dumpOfMatrix (double[][] matrix) + { + StringBuilder sb = new StringBuilder(); + + for (int col = 0; col < matrix[0].length; col++) { + sb.append(String.format("%14d", col)); + } + + sb.append(String.format("%n")); + + for (int row = 0; row < matrix.length; row++) { + sb.append(String.format("%2d:", row)); + + for (int col = 0; col < matrix[0].length; col++) { + sb.append(String.format("%14e", matrix[row][col])); + } + + sb.append(String.format("%n")); + } + + return sb.toString(); + } + + //---------// + // forward // + //---------// + /** + * Re-entrant method. + * + * @param ins input cells + * @param weights applied weights + * @param outs output cells + */ + private void forward (double[] ins, + double[][] weights, + double[] outs) + { + double sum; + double[] ws; + + for (int o = outs.length - 1; o >= 0; o--) { + sum = 0; + ws = weights[o]; + + for (int i = ins.length - 1; i >= 0; i--) { + sum += (ws[i + 1] * ins[i]); + } + + // Bias + sum += ws[0]; + + outs[o] = sigmoid(sum); + } + } + + //----------------// + // getJaxbContext // + //----------------// + private static JAXBContext getJaxbContext () + throws JAXBException + { + // Lazy creation + if (jaxbContext == null) { + jaxbContext = JAXBContext.newInstance(NeuralNetwork.class); + } + + return jaxbContext; + } + + //---------// + // sigmoid // + //---------// + /** + * Simple sigmoid function, with a step around 0 abscissa. + * + * @param val abscissa + * @return the related function value + */ + private double sigmoid (double val) + { + return 1.0d / (1.0d + Math.exp(-val)); + } + + //~ Inner Interfaces ------------------------------------------------------- + // + //---------// + // Monitor // + //---------// + /** + * Interface {@code Monitor} allows to plug a monitor to a Neural + * Network instance, and inform the monitor about the progress of + * the training activity. + */ + public static interface Monitor + { + //~ Methods ------------------------------------------------------------ + + /** + * Entry called at end of each epoch during the training phase. + * + * @param epochIndex the sequential index of completed epoch + * @param mse the remaining mean square error + */ + void epochEnded (int epochIndex, + double mse); + + /** + * Entry called at the beginning of the training phase, to allow + * initial snap shots for example. + * + * @param epochIndex the sequential index (0) + * @param mse the starting mean square error + * */ + void trainingStarted (final int epochIndex, + final double mse); + } + + //~ Inner Classes ---------------------------------------------------------- + // + //--------// + // Backup // + //--------// + /** + * Class {@code Backup} is an opaque class that encapsulates a + * snapshot of a NeuralNetwork internal memory (its weights). + * A Backup instance can only be obtained through the use of {@link #backup} + * method of a NeuralNetwork. + * A Backup instance is the needed parameter for a NeuralNetwork {@link + * #restore} action. + */ + public static class Backup + { + //~ Instance fields ---------------------------------------------------- + + private double[][] hiddenWeights; + + private double[][] outputWeights; + + //~ Constructors ------------------------------------------------------- + // Private constructor + private Backup (double[][] hiddenWeights, + double[][] outputWeights) + { + this.hiddenWeights = cloneMatrix(hiddenWeights); + this.outputWeights = cloneMatrix(outputWeights); + } + } +} diff --git a/src/main/omr/math/PointsCollector.java b/src/main/omr/math/PointsCollector.java new file mode 100644 index 0000000..999b128 --- /dev/null +++ b/src/main/omr/math/PointsCollector.java @@ -0,0 +1,182 @@ +//----------------------------------------------------------------------------// +// // +// P o i n t s C o l l e c t o r // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.math; + +import java.awt.Rectangle; +import java.util.Arrays; + +/** + * Class {@code PointsCollector} is meant to cumulate points + * coordinates, perhaps within a provided absolute region of + * interest. + */ +public class PointsCollector +{ + //~ Instance fields -------------------------------------------------------- + + /** The absolute region of interest, if any */ + private Rectangle roi; + + /** The current number of points in this collector */ + private int size; + + /** The abscissae */ + private int[] xx; + + /** The ordinates */ + private int[] yy; + + //~ Constructors ----------------------------------------------------------- + //-----------------// + // PointsCollector // + //-----------------// + /** + * Creates a new PointsCollector object, with absolute roi area + * taken as capacity. + * + * @param roi the absolute roi to be used by the collector + */ + public PointsCollector (Rectangle roi) + { + this(roi, roi.width * roi.height); + } + + //-----------------// + // PointsCollector // + //-----------------// + /** + * Creates a new PointsCollector object. + * + * @param roi the absolute roi to be used by the collector + * @param capacity the collector capacity + */ + public PointsCollector (Rectangle roi, + int capacity) + { + this.roi = roi; + xx = new int[capacity]; + yy = new int[capacity]; + } + + //~ Methods ---------------------------------------------------------------- + //----------// + // getSize // + //----------// + /** + * Report the current number of points collected. + * + * @return the current number of points + */ + public final int getSize () + { + return size; + } + + //--------// + // getRoi // + //--------// + /** + * Report the absolute region of interest for this collector + * + * @return the related ROI if any, null otherwise + */ + public Rectangle getRoi () + { + return roi; + } + + //------------// + // getXValues // + //------------// + /** + * Report the current abscissae. + * + * @return sequence of abscissae + */ + public final int[] getXValues () + { + return xx; + } + + //------------// + // getYValues // + //------------// + /** + * Report the current ordinates. + * + * @return sequence of ordinates + */ + public final int[] getYValues () + { + return yy; + } + + //---------// + // include // + //---------// + /** + * Include one point (while increasing capacity if needed). + * + * @param x point abscissa + * @param y point ordinate + */ + public final void include (int x, + int y) + { + ensureCapacity(size + 1); + xx[size] = x; + yy[size] = y; + size++; + } + + //----------------// + // ensureCapacity // + //----------------// + /** + * Increases the capacity of this instance. + * + * @param minCapacity the desired minimum capacity + */ + public void ensureCapacity (int minCapacity) + { + int oldCapacity = xx.length; + + if (minCapacity > oldCapacity) { + int newCapacity = ((oldCapacity * 3) / 2) + 1; + + if (newCapacity < minCapacity) { + newCapacity = minCapacity; + } + + xx = Arrays.copyOf(xx, newCapacity); + yy = Arrays.copyOf(yy, newCapacity); + } + } + + //----------// + // toString // + //----------// + @Override + public String toString () + { + StringBuilder sb = new StringBuilder(); + + sb.append("{") + .append(getClass().getSimpleName()) + .append(" size:") + .append(size); + + sb.append("}"); + + return sb.toString(); + } +} diff --git a/src/main/omr/math/Polynomial.java b/src/main/omr/math/Polynomial.java new file mode 100644 index 0000000..9505e77 --- /dev/null +++ b/src/main/omr/math/Polynomial.java @@ -0,0 +1,346 @@ +//----------------------------------------------------------------------------// +// // +// P o l y n o m i a l // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.math; + +/** + * Class {@code Polynomial} is a simple polynomial implementation. + * + * See http://introcs.cs.princeton.edu/java/92symbolic/Polynomial.java.html + * + * @author Hervé Bitteur + */ +public class Polynomial +{ + //~ Instance fields -------------------------------------------------------- + + /** The degree of polynomial */ + protected int degree; + + /** Polynomial coefficient vector, from low to high order */ + protected double[] coefficients; + + //~ Constructors ----------------------------------------------------------- + //------------// + // Polynomial // + //------------// + /** + * Creates a new Polynomial object (actually just a monomial). + * Example new Polynomial(3,2) = 3x^2 + * + * @param c coefficient + * @param degree degree of the monomial term + */ + public Polynomial (double c, + int degree) + { + if (degree < 0) { + throw new IllegalArgumentException("Negative polynomial degree"); + } + + coefficients = new double[degree + 1]; + coefficients[degree] = c; + this.degree = degree(); + } + + //~ Methods ---------------------------------------------------------------- + //---------// + // compose // + //---------// + /** + * Compose with that other polynomial. + * + * @param that the other polynomial + * @return a(b(x)) + */ + public Polynomial compose (Polynomial that) + { + Polynomial result = new Polynomial(0, 0); + + for (int i = this.degree; i >= 0; i--) { + Polynomial term = new Polynomial(this.coefficients[i], 0); + result = term.plus(that.times(result)); + } + + return result; + } + + //--------// + // degree // + //--------// + /** + * Report the actual degree. + * + * @return the degree of this polynomial (0 for the zero polynomial) + */ + public final int degree () + { + for (int i = coefficients.length - 1; i >= 0; i--) { + if (coefficients[i] != 0) { + return i; + } + } + + return 0; + } + + //------------// + // derivative // + //------------// + /** + * Report the derivative of this polynomial. + * + * @return the derivative + */ + public Polynomial derivative () + { + if (degree == 0) { + return new Polynomial(0, 0); + } + + Polynomial derivative = new Polynomial(0, degree - 1); + derivative.degree = degree - 1; + + for (int i = 0; i < degree; i++) { + derivative.coefficients[i] = (i + 1) * coefficients[i + 1]; + } + + return derivative; + } + + //----// + // eq // + //----// + /** + * Check whether this represent the same polynomial as that. + * + * @param that the other polynomial + * @return true if equal + */ + public boolean eq (Polynomial that) + { + if (degree != that.degree) { + return false; + } + + for (int i = degree; i >= 0; i--) { + if (coefficients[i] != that.coefficients[i]) { + return false; + } + } + + return true; + } + + //----------// + // evaluate // + //----------// + /** + * Return the evaluation of this polynomial for the provided x. + * + * @param x the provided x + * @return value at x + */ + public double evaluate (double x) + { + // use Horner's method + double result = 0; + + for (int i = degree; i >= 0; i--) { + result = coefficients[i] + (x * result); + } + + return result; + } + + //------// + // main // + //------// + // test client + public static void main (String[] args) + { + Polynomial zero = new Polynomial(0, 0); + + Polynomial p1 = new Polynomial(4, 3); // 4x^3 + Polynomial p2 = new Polynomial(3, 2); // 3x^2 + Polynomial p3 = new Polynomial(1, 0); // 1 + Polynomial p4 = new Polynomial(2, 1); // 2x + Polynomial p = p1.plus(p2) + .plus(p3) + .plus(p4); // 4x^3 + 3x^2 + 2x + 1 + + Polynomial q1 = new Polynomial(3, 2); // 3x^2 + Polynomial q2 = new Polynomial(5, 0); // 5 + Polynomial q = q1.plus(q2); // 3x^2 + 5 + + Polynomial r = p.plus(q); + Polynomial s = p.times(q); + Polynomial t = p.compose(q); + + System.out.println("zero(x) = " + zero); + System.out.println("p(x) = " + p); + System.out.println("q(x) = " + q); + System.out.println("p(x) + q(x) = " + r); + System.out.println("p(x) * q(x) = " + s); + System.out.println("p(q(x)) = " + t); + System.out.println("0 - p(x) = " + zero.minus(p)); + System.out.println("p(3) = " + p.evaluate(3)); + System.out.println("p'(x) = " + p.derivative()); + System.out.println("p''(x) = " + p.derivative().derivative()); + System.out.println( + "p'''(x) = " + p.derivative().derivative().derivative()); + System.out.println( + "p''''(x) = " + + p.derivative().derivative().derivative().derivative()); + } + + //-------// + // minus // + //-------// + /** + * Report this - that. + * + * @param that other polynomial + * @return this - that + */ + public Polynomial minus (Polynomial that) + { + Polynomial result = new Polynomial( + 0, + Math.max(this.degree, that.degree)); + + for (int i = 0; i <= this.degree; i++) { + result.coefficients[i] += this.coefficients[i]; + } + + for (int i = 0; i <= that.degree; i++) { + result.coefficients[i] -= that.coefficients[i]; + } + + result.degree = result.degree(); + + return result; + } + + //------// + // plus // + //------// + /** + * Report this + that. + * + * @param that other polynomial + * @return this + that + */ + public Polynomial plus (Polynomial that) + { + Polynomial result = new Polynomial( + 0, + Math.max(this.degree, that.degree)); + + for (int i = 0; i <= this.degree; i++) { + result.coefficients[i] += this.coefficients[i]; + } + + for (int i = 0; i <= that.degree; i++) { + result.coefficients[i] += that.coefficients[i]; + } + + result.degree = result.degree(); + + return result; + } + + //-------// + // times // + //-------// + /** + * Report this * that. + * + * @param that other polynomial + * @return this * that + */ + public Polynomial times (Polynomial that) + { + Polynomial result = new Polynomial(0, this.degree + that.degree); + + for (int i = 0; i <= this.degree; i++) { + for (int j = 0; j <= that.degree; j++) { + result.coefficients[i + j] += (this.coefficients[i] * that.coefficients[j]); + } + } + + result.degree = result.degree(); + + return result; + } + + //-------// + // times // + //-------// + /** + * Simple multiplication by a scalar + * + * @param scalar the scalar multiplicator value + * @return the new polynomial (this * scalar) + */ + public Polynomial times (double scalar) + { + Polynomial result = new Polynomial(0, degree); + + for (int i = 0; i <= degree; i++) { + result.coefficients[i] = coefficients[i] * scalar; + } + + result.degree = result.degree(); + + return result; + } + + //----------// + // toString // + //----------// + /** + * Print by decreasing term degree. + * + * @return the polynomial terms, presented by decreasing degree + */ + @Override + public String toString () + { + if (degree == 0) { + return "" + coefficients[0]; + } + + if (degree == 1) { + return coefficients[1] + "x + " + coefficients[0]; + } + + String s = coefficients[degree] + "x^" + degree; + + for (int i = degree - 1; i >= 0; i--) { + if (coefficients[i] == 0) { + continue; + } else if (coefficients[i] > 0) { + s = s + " + " + (coefficients[i]); + } else if (coefficients[i] < 0) { + s = s + " - " + (-coefficients[i]); + } + + if (i == 1) { + s += "x"; + } else if (i > 1) { + s = s + "x^" + i; + } + } + + return s; + } +} diff --git a/src/main/omr/math/Population.java b/src/main/omr/math/Population.java new file mode 100644 index 0000000..9b6f3ec --- /dev/null +++ b/src/main/omr/math/Population.java @@ -0,0 +1,191 @@ +//----------------------------------------------------------------------------// +// // +// P o p u l a t i o n // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.math; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlAttribute; +import javax.xml.bind.annotation.XmlRootElement; + +/** + * Class {@code Population} is used to cumulate measurements, + * and compute mean value, standard deviation and variance on them. + * + * @author Hervé Bitteur + */ +@XmlAccessorType(XmlAccessType.NONE) +@XmlRootElement(name = "population") +public class Population +{ + //~ Instance fields -------------------------------------------------------- + + /** Sum of measured values */ + @XmlAttribute(name = "sum") + private double s = 0d; + + /** Sum of squared measured values */ + @XmlAttribute(name = "squares-sum") + private double s2 = 0d; + + /** Number of measurements */ + @XmlAttribute(name = "count") + private int n = 0; + + //~ Constructors ----------------------------------------------------------- + //------------// + // Population // + //------------// + /** + * Construct a structure to cumulate the measured values + */ + public Population () + { + } + + //~ Methods ---------------------------------------------------------------- + //--------------// + // excludeValue // + //--------------// + /** + * Remove a measurement from the cumulated values + * + * @param val the measure value to remove + */ + public void excludeValue (double val) + { + if (n < 1) { + throw new RuntimeException("Population is empty"); + } + + n -= 1; + s -= val; + s2 -= (val * val); + } + + //----------------// + // getCardinality // + //----------------// + /** + * Get the number of cumulated measurements + * + * @return this number + */ + public int getCardinality () + { + return n; + } + + //--------------// + // getMeanValue // + //--------------// + /** + * Retrieve the mean value from the measurements cumulated so far + * + * @return the mean value + */ + public double getMeanValue () + { + if (n == 0) { + throw new RuntimeException("Population is empty"); + } + + return s / (double) n; + } + + //----------------------// + // getStandardDeviation // + //----------------------// + /** + * Get the standard deviation around the mean value + * + * @return the standard deviation + */ + public double getStandardDeviation () + { + return Math.sqrt(getVariance()); + } + + //-------------// + // getVariance // + //-------------// + /** + * Get the variance around the mean value + * + * @return the variance (square of standard deviation) + */ + public double getVariance () + { + if (n < 2) { + throw new RuntimeException("Not enough cumulated values : " + n); + } + + ///return Math.max(0d, (s2 - ((s * s) / n)) / (n - 1)); // Unbiased + return Math.max(0d, (s2 - ((s * s) / n)) / n); // Biased + } + + //-------------------// + // includePopulation // + //-------------------// + /** + * Add a whole population to this one + * + * @param other the other population to include + */ + public void includePopulation (Population other) + { + n += other.n; + s += other.s; + s2 += other.s2; + } + + //--------------// + // includeValue // + //--------------// + /** + * Add a measurement to the cumulated values + * + * @param val the measure value + */ + public void includeValue (double val) + { + n += 1; + s += val; + s2 += (val * val); + } + + //-------// + // reset // + //-------// + /** + * Forget all measurements made so far. + */ + public void reset () + { + n = 0; + s = 0d; + s2 = 0d; + } + + //-------// + // reset // + //-------// + /** + * Reset to the single measurement provided + * + * @param val the new first measured value + */ + public void reset (double val) + { + reset(); + includeValue(val); + } +} diff --git a/src/main/omr/math/Rational.java b/src/main/omr/math/Rational.java new file mode 100644 index 0000000..c21a29d --- /dev/null +++ b/src/main/omr/math/Rational.java @@ -0,0 +1,378 @@ +//----------------------------------------------------------------------------// +// // +// R a t i o n a l // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.math; + +import java.math.BigInteger; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlAttribute; +import javax.xml.bind.annotation.XmlRootElement; + +/** + * Class {@code Rational} implements non-mutable rational numbers + * (composed of a numerator and a denominator). + * + *

Invariants:

    + *
  1. The rational data is always kept in reduced form : gcd(num,den)==1
  2. + *
  3. The denominator value is always kept positive : den >= 1
  4. + *

+ * + *

It is (un)marshallable through JAXB.

+ * + * @author Hervé Bitteur + */ +@XmlAccessorType(XmlAccessType.NONE) +@XmlRootElement(name = "rational") +public class Rational + implements Comparable +{ + //~ Static fields/initializers --------------------------------------------- + + /** The zero rational instance */ + public static final Rational ZERO = new Rational(0, 1); + + /** The one rational instance */ + public static final Rational ONE = new Rational(1, 1); + + /** Max rational value */ + public static final Rational MAX_VALUE = new Rational(Integer.MAX_VALUE, 1); + + //~ Instance fields -------------------------------------------------------- + /** Final denominator value */ + @XmlAttribute + public final int den; + + /** Final numerator value */ + @XmlAttribute + public final int num; + + //~ Constructors ----------------------------------------------------------- + //----------// + // Rational // + //----------// + /** + * Create a final Rational instance + * + * @param num numerator value + * @param den denominator value + * @throws IllegalArgumentException if the provided denominator is zero + */ + public Rational (int num, + int den) + { + if (den == 0) { + throw new IllegalArgumentException("Denominator is zero"); + } + + // Reduction + int gcd = GCD.gcd(num, den); + num /= gcd; + den /= gcd; + + // Positive denominator + if (den < 0) { + den = -den; + num = -num; + } + + // Record final values + this.num = num; + this.den = den; + } + + //----------// + // Rational // + //----------// + /** Needed for JAXB */ + private Rational () + { + num = den = 1; + } + + //~ Methods ---------------------------------------------------------------- + //-----// + // abs // + //-----// + /** + * Report the absolute value + * + * @return |num| / den + */ + public Rational abs () + { + return new Rational(Math.abs(num), den); + } + + //-----------// + // compareTo // + //-----------// + /** + * Comparison + * + * @param that the other rational instance + * @return -1,0,1 if this <,==,> that respectively + */ + @Override + public int compareTo (Rational that) + { + int a = this.num * that.den; + int b = this.den * that.num; + + // Detect overflow, using the fact that den's are always >= 1 + if ((Integer.signum(b) != Integer.signum(that.num)) + || (Integer.signum(a) != Integer.signum(this.num))) { + BigInteger bigThisNum = BigInteger.valueOf(this.num); + BigInteger bigThisDen = BigInteger.valueOf(this.den); + BigInteger bigThatNum = BigInteger.valueOf(that.num); + BigInteger bigThatDen = BigInteger.valueOf(that.den); + BigInteger A = bigThisNum.multiply(bigThatDen); + BigInteger B = bigThisDen.multiply(bigThatNum); + + return A.compareTo(B); + } else { + return Integer.signum(a - b); + } + + ///return Integer.signum((this.num * that.den) - (this.den * that.num)); + } + + //---------// + // divides // + //---------// + /** + * Division + * + * @param that the other rational instance + * @return this / that + */ + public Rational divides (Rational that) + { + return times(that.inverse()); + } + + //---------// + // divides // + //---------// + /** + * Division + * + * @param that the integer to divide by + * @return this / that + */ + public Rational divides (int that) + { + return new Rational(num, den * that); + } + + //--------// + // equals // + //--------// + /** + * Identity + * + * @param obj the instance to compare to + * @return true if this value equals that value + */ + @Override + public boolean equals (Object obj) + { + if (!(obj instanceof Rational)) { + return false; + } else { + return compareTo((Rational) obj) == 0; + } + } + + //-----// + // gcd // + //-----// + public static Rational gcd (Rational a, + Rational b) + { + if (a.num == 0) { + return b; + } else { + return new Rational(1, GCD.lcm(a.den, b.den)); + } + } + + //-----// + // gcd // + //-----// + public static Rational gcd (Rational... vals) + { + Rational s = Rational.ZERO; + + for (Rational val : vals) { + s = gcd(s, val); + } + + return s; + } + + //----------// + // hashCode // + //----------// + /** {@inheritDoc } */ + @Override + public int hashCode () + { + int hash = 5; + hash = (89 * hash) + den; + hash = (89 * hash) + num; + + return hash; + } + + //---------// + // inverse // + //---------// + /** + * Unary inversion + * + * @return 1 / this + */ + public Rational inverse () + { + return new Rational(den, num); + } + + //-------// + // minus // + //-------// + /** + * Substraction + * + * @param that the other rational instance + * @return this - that + */ + public Rational minus (Rational that) + { + return plus(that.opposite()); + } + + //-------// + // minus // + //-------// + /** + * Substraction + * + * @param that the integer to substract + * @return this - that + */ + public Rational minus (int that) + { + return plus(-that); + } + + //----------// + // opposite // + //----------// + /** + * Unary negation + * + * @return -this + */ + public Rational opposite () + { + return new Rational(-num, den); + } + + //------// + // plus // + //------// + /** + * Addition + * + * @param that the other rational instance + * @return this + that + */ + public Rational plus (Rational that) + { + if (this.equals(ZERO)) { + return that; + } + + if (that.equals(ZERO)) { + return this; + } + + return new Rational( + (this.num * that.den) + (this.den * that.num), + this.den * that.den); + } + + //------// + // plus // + //------// + /** + * Addition + * + * @param that the integer to add + * @return this + that + */ + public Rational plus (int that) + { + return plus(new Rational(that, 1)); + } + + //-------// + // times // + //-------// + /** + * Multiplication + * + * @param that the other rational instance + * @return this * that + */ + public Rational times (Rational that) + { + return new Rational(this.num * that.num, this.den * that.den); + } + + //-------// + // times // + //-------// + /** + * Multiplication + * + * @param that the integer to multiply by + * @return this * that + */ + public Rational times (int that) + { + return new Rational(num * that, den); + } + + //----------// + // toDouble // + //----------// + public double toDouble () + { + return (double) num / den; + } + + //----------// + // toString // + //----------// + /** {@inheritDoc } */ + @Override + public String toString () + { + if (den == 1) { + return num + ""; + } else { + return num + "/" + den; + } + } +} diff --git a/src/main/omr/math/ReversePathIterator.java b/src/main/omr/math/ReversePathIterator.java new file mode 100644 index 0000000..4033720 --- /dev/null +++ b/src/main/omr/math/ReversePathIterator.java @@ -0,0 +1,492 @@ +//----------------------------------------------------------------------------// +// // +// R e v e r s e P a t h I t e r a t o r // +// // +//----------------------------------------------------------------------------// +package omr.math; + +// ============================================================================ +// File: ReversePathIterator.java +// +// Project: General utilities. +// +// Purpose: Is missing in java.awt.geom. +// +// Author: Rammi +// +// Copyright Notice: (c) 2005 Rammi (rammi@caff.de) +// This code is in the public domain. +// This usage of this code is allowed without any restrictions. +// No guarantees are given whatsoever. +// +// Latest change: $Date: 2005/02/08 11:59:19 $ +// +// History: $Log: ReversePathIterator.java,v $ +// History: Revision 1.3 2005/02/08 11:59:19 rammi +// History: Transferred into the public domain. +// History: +// History: Revision 1.2 2005/02/07 18:58:45 rammi +// History: Added optimizations +// History: +// History: Revision 1.1 2005/01/25 14:17:20 rammi +// History: First version +// History: +//============================================================================= +import java.awt.Shape; +import java.awt.geom.AffineTransform; +import java.awt.geom.IllegalPathStateException; +import java.awt.geom.PathIterator; + +/** + * A path iterator which iterates over a path in the reverse direction. + * This is missing in the java.awt.geom package, although it's quite simple to + * implement. + * After initialization the original PathIterator is not used any longer. + *

+ * There are several static convenience methods to create a reverse path + * iterator from + * a shape directly: + *

    + *
  • {@link #getReversePathIterator(java.awt.Shape)} + * for reversing the standard path iterator
  • + *
  • {@link #getReversePathIterator(java.awt.Shape, double)} + * for reversing a flattened path iterator
  • + *
  • {@link #getReversePathIterator(java.awt.Shape, java.awt.geom.AffineTransform)} + * for reversing a transformed path iterator
  • + *
  • {@link #getReversePathIterator(java.awt.Shape, java.awt.geom.AffineTransform, double)} + * for reversing a transformed flattened path iterator
  • + *
  • {@link #getReversePathIterator(java.awt.Shape, int)} + * for reversing the standard path iterator while explicitely defining a winding + * rule
  • + *
  • {@link #getReversePathIterator(java.awt.Shape, double, int)} + * for reversing a flattened path iterator while explicitely defining a winding + * rule
  • + *
  • {@link #getReversePathIterator(java.awt.Shape, java.awt.geom.AffineTransform, int)} + * for reversing a transformed path iterator while explicitely defining a + * winding rule
  • + *
  • {@link #getReversePathIterator(java.awt.Shape, java.awt.geom.AffineTransform, double, int)} + * for reversing a transformed flattened path iterator while explicitely + * defining a winding rule
  • + *
+ * + * @author Rammi + * @version $Revision: 1.3 $ + */ +public class ReversePathIterator + implements PathIterator +{ + //~ Instance fields -------------------------------------------------------- + + /** The winding rule. */ + private final int windingRule; + + /** The reversed coordinates. */ + private final double[] coordinates; + + /** The reversed segment types. */ + private final int[] segmentTypes; + + /** The index into the coordinates during iteration. */ + private int coordIndex = 0; + + /** The index into the segment types during iteration. */ + private int segmentIndex = 0; + + //~ Constructors ----------------------------------------------------------- + /** + * Create an inverted path iterator from a standard one, keeping the winding + * rule. + * + * @param original original iterator + */ + public ReversePathIterator (PathIterator original) + { + this(original, original.getWindingRule()); + } + + /** + * Create an inverted path iterator from a standard one. + * + * @param original original iterator + * @param windingRule winding rule of newly created iterator + */ + public ReversePathIterator (PathIterator original, + int windingRule) + { + this.windingRule = windingRule; + + double[] coords = new double[16]; + int coordPos = 0; + int[] segTypes = new int[8]; + int segPos = 0; + boolean first = true; + + double[] temp = new double[6]; + + while (!original.isDone()) { + if (segPos == segTypes.length) { + // resize + int[] dummy = new int[2 * segPos]; + System.arraycopy(segTypes, 0, dummy, 0, segPos); + segTypes = dummy; + } + + final int segType = segTypes[segPos++] = original.currentSegment( + temp); + + if (first) { + if (segType != SEG_MOVETO) { + throw new IllegalPathStateException( + "missing initial moveto in path definition"); + } + + first = false; + } + + int copy; + + switch (segType) { + case SEG_MOVETO: + case SEG_LINETO: + copy = 2; + + break; + + case SEG_QUADTO: + copy = 4; + + break; + + case SEG_CUBICTO: + copy = 6; + + break; + + default: + copy = 0; + + break; + } + + if (copy > 0) { + if ((coordPos + copy) > coords.length) { + // resize + double[] dummy = new double[coords.length * 2]; + System.arraycopy(coords, 0, dummy, 0, coords.length); + coords = dummy; + } + + for (int c = 0; c < copy; ++c) { + coords[coordPos++] = temp[c]; + } + } + + original.next(); + } + + // === reverse everything === + // --- reverse coordinates --- + coordinates = new double[coordPos]; + + for (int p = (coordPos / 2) - 1; p >= 0; --p) { + coordinates[2 * p] = coords[coordPos - (2 * p) - 2]; + coordinates[(2 * p) + 1] = coords[coordPos - (2 * p) - 1]; + } + + // --- reverse segment types --- + segmentTypes = new int[segPos]; + + if (segPos > 0) { + boolean pendingClose = false; + int sr = 0; + segmentTypes[sr++] = SEG_MOVETO; + + for (int s = segPos - 1; s > 0; --s) { + switch (segTypes[s]) { + case SEG_MOVETO: + + if (pendingClose) { + pendingClose = false; + segmentTypes[sr++] = SEG_CLOSE; + } + + segmentTypes[sr++] = SEG_MOVETO; + + break; + + case SEG_CLOSE: + pendingClose = true; + + break; + + default: + segmentTypes[sr++] = segTypes[s]; + + break; + } + } + + if (pendingClose) { + segmentTypes[sr] = SEG_CLOSE; + } + } + } + + //~ Methods ---------------------------------------------------------------- + /** + * Get a reverse path iterator for a shape, keeping the shape's winding + * rule. + * + * @param shape shape for which a reverse path iterator is needed + * @return reverse path iterator + */ + public static PathIterator getReversePathIterator (Shape shape) + { + return new ReversePathIterator(shape.getPathIterator(null)); + } + + /** + * Get a reverse flattened path iterator for a shape, keeping the shape's + * winding rule. + * + * @param shape shape for which a reverse flattened path iterator is + * needed + * @param flatness flatness epsilon + * @return reverse flattened path iterator + */ + public static PathIterator getReversePathIterator (Shape shape, + double flatness) + { + return new ReversePathIterator(shape.getPathIterator(null, flatness)); + } + + /** + * Get a reverse transformed path iterator for a shape, keeping the shape's + * winding rule. + * + * @param shape shape for which a reverse transformed path iterator is + * needed + * @return reverse transformed path iterator + */ + public static PathIterator getReversePathIterator (Shape shape, + AffineTransform at) + { + return new ReversePathIterator(shape.getPathIterator(at)); + } + + /** + * Get a reverse transformed flattened path iterator for a shape, keeping + * the shape's winding rule. + * + * @param shape shape for which a reverse transformed flattened path + * iterator is needed + * @param flatness flatness epsilon + * @return reverse transformed flattened path iterator + */ + public static PathIterator getReversePathIterator (Shape shape, + AffineTransform at, + double flatness) + { + return new ReversePathIterator(shape.getPathIterator(at, flatness)); + } + + /** + * Get a reverse path iterator for a shape. + * + * @param shape shape for which a reverse path iterator is needed + * @param windingRule winding rule of newly created iterator + * @return reverse path iterator + */ + public static PathIterator getReversePathIterator (Shape shape, + int windingRule) + { + return new ReversePathIterator( + shape.getPathIterator(null), + windingRule); + } + + /** + * Get a reverse flattened path iterator for a shape. + * + * @param shape shape for which a reverse flattened path iterator is + * needed + * @param flatness flatness epsilon + * @param windingRule winding rule of newly created iterator + * @return reverse flattened path iterator + */ + public static PathIterator getReversePathIterator (Shape shape, + double flatness, + int windingRule) + { + return new ReversePathIterator( + shape.getPathIterator(null, flatness), + windingRule); + } + + /** + * Get a reverse transformed path iterator for a shape. + * + * @param shape shape for which a reverse transformed path iterator is + * needed + * @param windingRule winding rule of newly created iterator + * @return reverse transformed path iterator + */ + public static PathIterator getReversePathIterator (Shape shape, + AffineTransform at, + int windingRule) + { + return new ReversePathIterator(shape.getPathIterator(at), windingRule); + } + + /** + * Get a reverse transformed flattened path iterator for a shape. + * + * @param shape shape for which a reverse transformed flattened path + * iterator is needed + * @param flatness flatness epsilon + * @param windingRule winding rule of newly created iterator + * @return reverse transformed flattened path iterator + */ + public static PathIterator getReversePathIterator (Shape shape, + AffineTransform at, + double flatness, + int windingRule) + { + return new ReversePathIterator( + shape.getPathIterator(at, flatness), + windingRule); + } + + /** + * Returns the coordinates and type of the current path segment in + * the iteration. + * The return value is the path-segment type: + * SEG_MOVETO, SEG_LINETO, SEG_QUADTO, SEG_CUBICTO, or SEG_CLOSE. + * A double array of length 6 must be passed in and can be used to + * store the coordinates of the point(s). + * Each point is stored as a pair of double x,y coordinates. + * SEG_MOVETO and SEG_LINETO types returns one point, + * SEG_QUADTO returns two points, + * SEG_CUBICTO returns 3 points + * and SEG_CLOSE does not return any points. + * + * @param coords an array that holds the data returned from + * this method + * @return the path-segment type of the current path segment. + * @see #SEG_MOVETO + * @see #SEG_LINETO + * @see #SEG_QUADTO + * @see #SEG_CUBICTO + * @see #SEG_CLOSE + */ + @Override + public int currentSegment (double[] coords) + { + final int segmentType = segmentTypes[segmentIndex]; + final int copy = coordinatesForSegmentType(segmentType); + + if (copy > 0) { + System.arraycopy(coordinates, coordIndex, coords, 0, copy); + } + + return segmentType; + } + + /** + * Returns the coordinates and type of the current path segment in + * the iteration. + * The return value is the path-segment type: + * SEG_MOVETO, SEG_LINETO, SEG_QUADTO, SEG_CUBICTO, or SEG_CLOSE. + * A float array of length 6 must be passed in and can be used to + * store the coordinates of the point(s). + * Each point is stored as a pair of float x,y coordinates. + * SEG_MOVETO and SEG_LINETO types returns one point, + * SEG_QUADTO returns two points, + * SEG_CUBICTO returns 3 points + * and SEG_CLOSE does not return any points. + * + * @param coords an array that holds the data returned from + * this method + * @return the path-segment type of the current path segment. + * @see #SEG_MOVETO + * @see #SEG_LINETO + * @see #SEG_QUADTO + * @see #SEG_CUBICTO + * @see #SEG_CLOSE + */ + @Override + public int currentSegment (float[] coords) + { + final int segmentType = segmentTypes[segmentIndex]; + final int copy = coordinatesForSegmentType(segmentType); + + if (copy > 0) { + for (int c = 0; c < copy; ++c) { + coords[c] = (float) coordinates[coordIndex + c]; + } + } + + return segmentType; + } + + /** + * Returns the winding rule for determining the interior of the + * path. This just returns the winding rule of the original path, + * which may or may not be what is wanted. + * + * @return the winding rule. + * @see #WIND_EVEN_ODD + * @see #WIND_NON_ZERO + */ + @Override + public int getWindingRule () + { + return windingRule; + } + + /** + * Tests if the iteration is complete. + * + * @return {@code true} if all the segments have + * been read; {@code false} otherwise. + */ + @Override + public boolean isDone () + { + return segmentIndex >= segmentTypes.length; + } + + /** + * Moves the iterator to the next segment of the path forwards + * along the primary direction of traversal as long as there are + * more points in that direction. + */ + @Override + public void next () + { + coordIndex += coordinatesForSegmentType(segmentTypes[segmentIndex++]); + } + + /** + * Get the number of coordinates needed for a segment type. + * + * @param segtype segment type + * @return coordinates needed + */ + private static int coordinatesForSegmentType (int segtype) + { + switch (segtype) { + case SEG_MOVETO: + case SEG_LINETO: + return 2; + + case SEG_QUADTO: + return 4; + + case SEG_CUBICTO: + return 6; + } + + return 0; + } +} diff --git a/src/main/omr/math/package.html b/src/main/omr/math/package.html new file mode 100644 index 0000000..57b5665 --- /dev/null +++ b/src/main/omr/math/package.html @@ -0,0 +1,17 @@ + + + + + + Package omr.math + + + +

+ This package is a basic collection of mathematics constructions + such as lines, regressions, neural networks. +

+ + + diff --git a/src/main/omr/moments/ARTMoments.java b/src/main/omr/moments/ARTMoments.java new file mode 100644 index 0000000..0d5d966 --- /dev/null +++ b/src/main/omr/moments/ARTMoments.java @@ -0,0 +1,117 @@ +//----------------------------------------------------------------------------// +// // +// A R T M o m e n t s // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.moments; + +/** + * Interface {@code ARTMoments} defines a region shape features + * descriptor based on Angular Radial Transform. + * + * See MPEG-7 Experimentation Model for the original C++ code + * + * @author Hervé Bitteur + */ +public interface ARTMoments + extends OrthogonalMoments +{ + //~ Static fields/initializers --------------------------------------------- + + /** Number of angular indixes */ + public static final int ANGULAR = 12; + + /** Number of radius indices */ + public static final int RADIAL = 3; + + //~ Methods ---------------------------------------------------------------- + /** + * Report the argument value for provided phase and radius indices. + * + * @param p phase index + * @param r radius index + * @return the argument double value [-PI..PI] + */ + double getArgument (int p, + int r); + + /** + * Report the imaginary value for provided phase and radius indices. + * + * @param p phase index + * @param r radius index + * @return the module double value [0..1] + */ + double getImag (int p, + int r); + + /** + * Report the module value for provided phase and radius indices. + * + * @param p phase index + * @param r radius index + * @return the module double value [0..1] + */ + double getModule (int p, + int r); + + /** + * Report the real value for provided phase and radius indices. + * + * @param p phase index + * @param r radius index + * @return the module double value [0..1] + */ + double getReal (int p, + int r); + + /** + * Set the argument value for provided phase and radius indices. + * + * @param p phase index + * @param r radius index + * @param value the argument double value [-PI..PI] + */ + void setArgument (int p, + int r, + double value); + + /** + * Set the imaginary value for provided phase and radius indices. + * + * @param p phase index + * @param r radius index + * @param value the element double value [0..1] + */ + void setImag (int p, + int r, + double value); + + /** + * Set the module value for provided phase and radius indices. + * + * @param p phase index + * @param r radius index + * @param value the element double value [0..1] + */ + void setModule (int p, + int r, + double value); + + /** + * Set the real value for provided phase and radius indices. + * + * @param p phase index + * @param r radius index + * @param value the element double value [0..1] + */ + void setReal (int p, + int r, + double value); +} diff --git a/src/main/omr/moments/AbstractExtractor.java b/src/main/omr/moments/AbstractExtractor.java new file mode 100644 index 0000000..059cb1f --- /dev/null +++ b/src/main/omr/moments/AbstractExtractor.java @@ -0,0 +1,141 @@ +//----------------------------------------------------------------------------// +// // +// A b s t r a c t E x t r a c t o r // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.moments; + +import java.awt.geom.Point2D; + +/** + * Class {@code AbstractExtractor} provides the basis for moments + * extraction. + * + * @param actual descriptor type + * + * @author Hervé Bitteur + */ +public abstract class AbstractExtractor> + implements MomentsExtractor +{ + //~ Instance fields -------------------------------------------------------- + + /** Input abscissae. */ + protected int[] xx; + + /** Input ordinates. */ + protected int[] yy; + + /** Image mass (number of foreground points). */ + protected int mass; + + /** Center of mass. */ + protected Point2D center; + + /** Image max radius around its mass center. */ + protected double radius; + + /** The target descriptor. */ + protected D descriptor; + + //~ Methods ---------------------------------------------------------------- + //---------// + // extract // + //---------// + @Override + public void extract (int[] xx, + int[] yy, + int mass) + { + // Check arguments + if ((xx == null) || (yy == null)) { + throw new IllegalArgumentException( + getClass().getSimpleName() + " cannot process a null array"); + } + + if ((mass <= 0) || (mass > xx.length) || (mass > yy.length)) { + throw new IllegalArgumentException( + getClass().getSimpleName() + " on inconsistent input"); + } + + if (descriptor == null) { + throw new IllegalArgumentException( + getClass().getSimpleName() + " has no target descriptor"); + } + + this.xx = xx; + this.yy = yy; + this.mass = mass; + + findCenterOfMass(); + findRadius(); + + extractMoments(); + } + + //---------------// + // setDescriptor // + //---------------// + @Override + public void setDescriptor (D descriptor) + { + this.descriptor = descriptor; + } + + //----------------// + // extractMoments // + //----------------// + /** + * Actual extraction core, to be provided by subclasses. + */ + protected abstract void extractMoments (); + + //------------------// + // findCenterOfMass // + //------------------// + /** + * Computer the image mass center coordinates. + */ + private void findCenterOfMass () + { + int m10 = 0; + int m01 = 0; + + for (int i = 0; i < mass; i++) { + m10 += xx[i]; + m01 += yy[i]; + } + + center = new Point2D.Double( + (double) m10 / (double) mass, + (double) m01 / (double) mass); + + ///System.out.println("center: " + center); + } + + //------------// + // findRadius // + //------------// + /** + * Compute the image contour, centered around its mass center. + */ + private void findRadius () + { + radius = Double.MIN_VALUE; + + for (int i = 0; i < mass; i++) { + double x = xx[i] - center.getX(); + double y = yy[i] - center.getY(); + radius = Math.max(radius, Math.abs(x)); + radius = Math.max(radius, Math.abs(y)); + } + + ///System.out.println("radius:" + radius); + } +} diff --git a/src/main/omr/moments/BasicARTExtractor.java b/src/main/omr/moments/BasicARTExtractor.java new file mode 100644 index 0000000..e15ca3e --- /dev/null +++ b/src/main/omr/moments/BasicARTExtractor.java @@ -0,0 +1,163 @@ +//----------------------------------------------------------------------------// +// // +// B a s i c A R T E x t r a c t o r // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.moments; + +import static omr.moments.ARTMoments.*; + +import java.awt.image.WritableRaster; + +/** + * Class {@code BasicARTExtractor} implements extraction + * of ART Moments. + * + * See MPEG-7 Experimentation Model for the original C++ code + * + * @author Hervé Bitteur + */ +public class BasicARTExtractor + extends AbstractExtractor +{ + //~ Static fields/initializers --------------------------------------------- + + // Zernike basis function radius + private static final int LUT_RADIUS = 50; + + /** Real values of ARTMoments basis function */ + private static final LUT[][] realLuts = new LUT[ANGULAR][RADIAL]; + + /** Imaginary values of ARTMoments basis function */ + private static final LUT[][] imagLuts = new LUT[ANGULAR][RADIAL]; + + static { + initLUT(); + } + + //~ Constructors ----------------------------------------------------------- + /** + * Creates a new BasicARTExtractor object and process + * the provided foreground points. + */ + public BasicARTExtractor () + { + } + + //~ Methods ---------------------------------------------------------------- + @Override + public void reconstruct (WritableRaster raster) + { + ///throw new UnsupportedOperationException("Not supported yet."); + } + + //----------------// + // extractMoments // + //----------------// + @Override + protected void extractMoments () + { + final LUT anyLut = realLuts[0][0]; // Just for template + final int lutRadius = anyLut.getRadius(); + final double centerX = center.getX(); + final double centerY = center.getY(); + + // Coefficients, real part & imaginary part + final double[][] coeffReal = new double[ANGULAR][RADIAL]; + final double[][] coeffImag = new double[ANGULAR][RADIAL]; + + for (int i = 0; i < mass; i++) { + // Map image coordinate to LUT coordinates + double x = xx[i] - centerX; + double y = yy[i] - centerY; + double lx = ((x * lutRadius) / radius) + lutRadius; + double ly = ((y * lutRadius) / radius) + lutRadius; + + // Summation of basis function + if (anyLut.contains(lx, ly)) { + for (int p = 0; p < ANGULAR; p++) { + for (int r = 0; r < RADIAL; r++) { + coeffReal[p][r] += realLuts[p][r].interpolate(lx, ly); + coeffImag[p][r] -= imagLuts[p][r].interpolate(lx, ly); + } + } + } + } + + // Save to descriptor + for (int p = 0; p < ANGULAR; p++) { + for (int r = 0; r < RADIAL; r++) { + double real = coeffReal[p][r] / mass; + double imag = coeffImag[p][r] / mass; + descriptor.setMoment(p, r, Math.hypot(imag, real)); + + // descriptor.setArgument(p, r, Math.atan2(imag, real)); + // descriptor.setReal(p, r, real); + // descriptor.setImag(p, r, imag); + } + } + } + + //---------// + // initLUT // + //---------// + /** + * Compute, once for all, the lookup table values. + */ + private static void initLUT () + { + // Allocate LUT's + for (int p = 0; p < ANGULAR; p++) { + for (int r = 0; r < RADIAL; r++) { + realLuts[p][r] = new BasicLUT(LUT_RADIUS); + imagLuts[p][r] = new BasicLUT(LUT_RADIUS); + } + } + + final LUT anyLut = realLuts[0][0]; // Just for template + final int lutSize = anyLut.getSize(); + final int lutRadius = anyLut.getRadius(); + + for (int x = 0; x < lutSize; x++) { + double tx = (x - lutRadius) / (double) lutRadius; // [-1..+1] + + for (int y = 0; y < lutSize; y++) { + double ty = (y - lutRadius) / (double) lutRadius; // [-1..+1] + double rad = Math.hypot(tx, ty); // [0..sqrt(2)] + + if (rad < 1) { + // We are within circle + double angle = Math.atan2(ty, tx); + + for (int p = 0; p < ANGULAR; p++) { + for (int r = 0; r < RADIAL; r++) { + double temp = Math.cos(rad * Math.PI * r); + realLuts[p][r].assign( + x, + y, + temp * Math.cos(angle * p)); + imagLuts[p][r].assign( + x, + y, + temp * Math.sin(angle * p)); + } + } + } else { + // We are on or outside circle + for (int p = 0; p < ANGULAR; p++) { + for (int r = 0; r < RADIAL; r++) { + realLuts[p][r].assign(x, y, 0); + imagLuts[p][r].assign(x, y, 0); + } + } + } + } + } + } +} diff --git a/src/main/omr/moments/BasicARTMoments.java b/src/main/omr/moments/BasicARTMoments.java new file mode 100644 index 0000000..e62c4ba --- /dev/null +++ b/src/main/omr/moments/BasicARTMoments.java @@ -0,0 +1,197 @@ +//----------------------------------------------------------------------------// +// // +// B a s i c A R T M o m e n t s // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.moments; + +/** + * Class {@code BasicARTMoments} implements a basic region-based + * shape descriptor. + * + * See MPEG-7 Experimentation Model for the original C++ code + * + * @author Hervé Bitteur + */ +public class BasicARTMoments + implements ARTMoments +{ + //~ Instance fields -------------------------------------------------------- + + /** Module values */ + private final double[][] modules = new double[ANGULAR][RADIAL]; + + /** Argument values */ + private final double[][] arguments = new double[ANGULAR][RADIAL]; + + /** Imaginary values */ + private final double[][] imags = new double[ANGULAR][RADIAL]; + + /** Real values */ + private final double[][] reals = new double[ANGULAR][RADIAL]; + + //~ Constructors ----------------------------------------------------------- + //------------------// + // BasicARTMoments // + //------------------// + /** + * Creates a new BasicARTMoments object. + */ + public BasicARTMoments () + { + } + + //~ Methods ---------------------------------------------------------------- + //-----------// + // getModule // + //-----------// + @Override + public final double getModule (int p, + int r) + { + return modules[p][r]; + } + + //-----------// + // setModule // + //-----------// + @Override + public final void setModule (int p, + int r, + double value) + { + modules[p][r] = value; + } + + //------------// + // distanceTo // + //------------// + /** + * Implements a Manhattan distance + * + * @param that the other ARTMoments descriptor + * @return the (Manhattan) distance + */ + @Override + public double distanceTo (ARTMoments that) + { + double distance = 0; + + for (int p = 0; p < ANGULAR; p++) { + for (int r = 0; r < RADIAL; r++) { + if ((p != 0) || (r != 0)) { + distance += Math.abs( + that.getModule(p, r) - getModule(p, r)); + + // distance += Math.abs(that.getReal(p, r) - getReal(p, r)); + // distance += Math.abs(that.getImag(p, r) - getImag(p, r)); + } + } + } + + return distance; + } + + //-------------// + // getArgument // + //-------------// + @Override + public double getArgument (int p, + int r) + { + return arguments[p][r]; + } + + @Override + public double getImag (int p, + int r) + { + return imags[p][r]; + } + + //-----------// + // getMoment // + //-----------// + @Override + public double getMoment (int m, + int n) + { + return getModule(m, n); + } + + @Override + public double getReal (int p, + int r) + { + return reals[p][r]; + } + + //-------------// + // setArgument // + //-------------// + @Override + public void setArgument (int p, + int r, + double value) + { + arguments[p][r] = value; + } + + @Override + public void setImag (int p, + int r, + double value) + { + imags[p][r] = value; + } + + //-----------// + // setMoment // + //-----------// + @Override + public void setMoment (int m, + int n, + double value) + { + setModule(m, n, value); + } + + @Override + public void setReal (int p, + int r, + double value) + { + reals[p][r] = value; + } + + //----------// + // toString // + //----------// + @Override + public String toString () + { + StringBuilder sb = new StringBuilder("{"); + + for (int p = 0; p < ANGULAR; p++) { + for (int r = 0; r < RADIAL; r++) { + if ((p != 0) || (r != 0)) { + if (sb.length() > 1) { + sb.append(" "); + } + + sb.append(String.format("%3.0f", 1000 * getMoment(p, r))); + } + } + } + + sb.append("}"); + + return sb.toString(); + } +} diff --git a/src/main/omr/moments/BasicLUT.java b/src/main/omr/moments/BasicLUT.java new file mode 100644 index 0000000..ac398e3 --- /dev/null +++ b/src/main/omr/moments/BasicLUT.java @@ -0,0 +1,147 @@ +//----------------------------------------------------------------------------// +// // +// B a s i c L U T // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.moments; + +/** + * Class {@code BasicLUT} is a straightforward LUT implementation. + * + * @author Hervé Bitteur + */ +public final class BasicLUT + implements LUT +{ + //~ Instance fields -------------------------------------------------------- + + /** LUT radius. */ + private final int RADIUS; + + /** LUT size (to implement arrays [-RADIUS, RADIUS]). */ + private final int SIZE; + + /** The table of values for each integer (x,y) location. */ + private final double[][] table; + + //~ Constructors ----------------------------------------------------------- + /** + * Creates a new BasicLUT object. + * + * @param radius the desired LUT radius for a [-radius .. radius] table. + */ + public BasicLUT (int radius) + { + if (radius <= 0) { + throw new IllegalArgumentException( + "Cannot allocate LUT with radius " + radius); + } + + this.RADIUS = radius; + SIZE = 1 + (2 * radius); + table = new double[SIZE][SIZE]; + } + + //~ Methods ---------------------------------------------------------------- + //--------// + // assign // + //--------// + @Override + public void assign (int x, + int y, + double value) + { + table[x][y] = value; + } + + //----------// + // contains // + //----------// + @Override + public boolean contains (double radius) + { + return radius < RADIUS; + } + + //----------// + // contains // + //----------// + @Override + public boolean contains (double x, + double y) + { + return (x >= 0) && (x < SIZE) && (y >= 0) && (y < SIZE); + } + + //-----------// + // getRadius // + //-----------// + @Override + public int getRadius () + { + return RADIUS; + } + + //---------// + // getSize // + //---------// + @Override + public int getSize () + { + return SIZE; + } + + //-------------// + // interpolate // + //-------------// + @Override + public double interpolate (double px, + double py) + { + // Integer coordinates, by truncating precise coordinates + final int x = (int) px; + final int y = (int) py; + + // Beware of point on LUT border + final int max = SIZE - 1; + + // Value at [x,y] + final double vxy = table[x][y]; + + if (x == max) { + if (y == max) { + return vxy; // v[x,y] + } else { + final double iy = py - y; + + return vxy + (iy * (table[x][y + 1] - vxy)); // v[x,py] + } + } else { + final double ix = px - x; + + // Value at [px,y] + final double vpxy = vxy + (ix * (table[x + 1][y] - vxy)); + + if (y == max) { + return vpxy; // v[px,y] + } else { + final double iy = py - y; + + // Value at [x,y+1] + final double vxy1 = table[x][y + 1]; + + // Value at [px, y+1] + final double vpxy1 = vxy1 + + (ix * (table[x + 1][y + 1] - vxy1)); + + return vpxy + (iy * (vpxy1 - vpxy)); // v[px,py] + } + } + } +} diff --git a/src/main/omr/moments/BasicLegendreExtractor.java b/src/main/omr/moments/BasicLegendreExtractor.java new file mode 100644 index 0000000..63a4f5e --- /dev/null +++ b/src/main/omr/moments/BasicLegendreExtractor.java @@ -0,0 +1,331 @@ +//----------------------------------------------------------------------------// +// // +// B a s i c L e g e n d r e E x t r a c t o r // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.moments; + +import omr.math.Polynomial; +import static omr.moments.LegendreMoments.*; + +import java.awt.image.WritableRaster; + +/** + * Class {@code BasicLegendreExtractor} implements extraction of + * Legendre moments. + * + * @author Hervé Bitteur + */ +public class BasicLegendreExtractor + extends AbstractExtractor +{ + //~ Static fields/initializers --------------------------------------------- + + /** Legendre polynomials. */ + private static final Polynomial[] P = generatePolynomials(); + + /** LUT's for Legendre basis function values. */ + private static final LUT[][] luts; + + static { + System.out.println("order:" + ORDER); + + /** Basis function LUT radius, if zero no LUT is used. */ + final int LUT_RADIUS = 50; + + if (LUT_RADIUS > 0) { + System.out.println("LUT_RADIUS:" + LUT_RADIUS); + luts = new LUT[ORDER + 1][ORDER + 1]; + + for (int m = 0; m <= ORDER; m++) { + for (int n = 0; n <= (ORDER - m); n++) { + luts[m][n] = new BasicLUT(LUT_RADIUS); + } + } + + initLUT(); + } else { + luts = null; + } + + ///checkOrthogonal(); + } + + //~ Constructors ----------------------------------------------------------- + //------------------------// + // BasicLegendreExtractor // + //------------------------// + /** + * Creates a new BasicLegendreExtractor object. + */ + public BasicLegendreExtractor () + { + } + + //~ Methods ---------------------------------------------------------------- + //-------------// + // reconstruct // + //-------------// + @Override + public void reconstruct (WritableRaster raster) + { + int size = raster.getHeight(); + + ///double[][] buf = new double[size][size]; + final int rad = size / 2; + final int[] ia = new int[1]; + double minVal = Double.MAX_VALUE; + double maxVal = Double.MIN_VALUE; + + for (int x = 0; x < size; x++) { + double tx = (x - rad) / (double) rad; + + for (int y = 0; y < size; y++) { + double ty = (y - rad) / (double) rad; + double val = 0; + + for (int m = 0; m <= ORDER; m++) { + for (int n = 0; n <= (ORDER - m); n++) { + double tau = Math.sqrt( + (((2 * m) + 1) * ((2 * n) + 1)) / 4d); + double moment = descriptor.getMoment(m, n); + double pm = P[m].evaluate(tx); + double pn = P[n].evaluate(ty); + double inc = tau * moment * pm * pn; + val += inc; + + // System.out.println( + // String.format( + // Locale.UK, + // "m:%02d n:%02d tau:%7.3f mom:%6.3f pmn:%6.3f pn:%6.3f inc:%6.3f val:%6.3f", + // m, + // n, + // tau, + // moment, + // pmn, + // pn, + // inc, + // val)); + } + } + + // buf[x][y] = val; + minVal = Math.min(minVal, val); + maxVal = Math.max(maxVal, val); + + int gray = Math.min( + 255, + Math.max(0, (int) Math.rint(val * 256))); + ia[0] = 255 - gray; + raster.setPixel(x, y, ia); + } + } + + System.out.println("minVal:" + minVal + " maxVal:" + maxVal); + + // // Normalize the gray levels (berk!) + // for (int x = 0; x < size; x++) { + // for (int y = 0; y < size; y++) { + // double val = buf[x][y]; + // int gray = (int) Math.rint((val / maxVal) * 255); + // gray = Math.min(255, Math.max(0, gray)); + // ia[0] = 255 - gray; + // raster.setPixel(x, y, ia); + // } + // } + } + + //----------------// + // extractMoments // + //----------------// + @Override + protected void extractMoments () + { + if (luts == null) { + // No use of LUTs + extractMomentsDirectly(); + + return; + } + + final double area = 1.0 / (radius * radius); + final double centerX = center.getX(); + final double centerY = center.getY(); + final LUT anyLut = luts[0][0]; // Just for template + final int lutRadius = anyLut.getRadius(); + + // Coefficients + final double[][] coeffs = new double[ORDER + 1][ORDER + 1]; + + for (int i = 0; i < mass; i++) { + // Map image coordinates to LUT coordinates + double x = xx[i] - centerX; + double y = yy[i] - centerY; + double lx = ((x * lutRadius) / radius) + lutRadius; + double ly = ((y * lutRadius) / radius) + lutRadius; + + // Summation of basis function + if (anyLut.contains(lx, ly)) { + for (int m = 0; m <= ORDER; m++) { + for (int n = 0; n <= (ORDER - m); n++) { + coeffs[m][n] += luts[m][n].interpolate(lx, ly); + } + } + } + } + + // Save to descriptor + for (int m = 0; m <= ORDER; m++) { + double mNorm = Math.sqrt(((2 * m) + 1) / 2.0); + + for (int n = 0; n <= (ORDER - m); n++) { + double nNorm = Math.sqrt(((2 * n) + 1) / 2.0); + descriptor.setMoment(m, n, coeffs[m][n] * area * mNorm * nNorm); + } + } + } + + //------------------------// + // extractMomentsDirectly // Not using LUT (so rather slow...) + //------------------------// + private void extractMomentsDirectly () + { + final double area = 1.0 / (radius * radius); + final double centerX = center.getX(); + final double centerY = center.getY(); + + for (int m = 0; m <= ORDER; m++) { + double mNorm = Math.sqrt(((2 * m) + 1) / 2.0); + + for (int n = 0; n <= (ORDER - m); n++) { + double nNorm = Math.sqrt(((2 * n) + 1) / 2.0); + double val = 0; + + for (int i = 0; i < mass; i++) { + // Map image coordinate to basis function coordinate + double x = xx[i] - centerX; + double y = yy[i] - centerY; + + double ix = x / radius; // [-1 .. +1] + ix = Math.min(1, Math.max(ix, -1)); + + double iy = y / radius; // [-1 .. +1] + iy = Math.min(1, Math.max(iy, -1)); + + // Summation of basis function + double inc = P[m].evaluate(ix) * P[n].evaluate(iy); + inc *= (mNorm * nNorm); + val += inc; + } + + // Fake image, using a filled square (to be removed) + // int r = 10; + // area = 1.0 / (r * r); + // + // for (int x = -r; x <= r; x++) { + // double ix = x / r; + // + // for (int y = -r; y <= r; y++) { + // double iy = y / r; + // double inc = P[m].evaluate(ix) * P[n].evaluate(iy); + // inc *= mNorm * nNorm; + // val += inc; + // } + // } + + // Save to descriptor + descriptor.setMoment(m, n, val * area); + } + } + } + + //---------------------// + // generatePolynomials // + //---------------------// + /** + * Generate all Legendre polynomials, iteratively up to ORDER. + * + * @return the array of polynomials + */ + private static Polynomial[] generatePolynomials () + { + Polynomial[] Q = new Polynomial[ORDER + 1]; + + Q[0] = new Polynomial(1, 0); + Q[1] = new Polynomial(1, 1); + + for (int n = 2; n <= ORDER; n++) { + Q[n] = Q[1].times(Q[n - 1]) + .times((2 * n) - 1) + .minus(Q[n - 2].times(n - 1)) + .times(1d / n); + } + + if (false) { + for (int n = 0; n <= ORDER; n++) { + System.out.println("P[" + n + "] = " + Q[n]); + } + } + + return Q; + } + + //---------// + // initLUT // + //---------// + /** + * Compute, once for all, the lookup table values. + */ + private static void initLUT () + { + final LUT anyLut = luts[0][0]; // Just for template + final int lutSize = anyLut.getSize(); + final int lutRadius = anyLut.getRadius(); + + for (int x = 0; x < lutSize; x++) { + double tx = (x - lutRadius) / (double) lutRadius; + + for (int y = 0; y < lutSize; y++) { + double ty = (y - lutRadius) / (double) lutRadius; + + for (int m = 0; m <= ORDER; m++) { + double pmx = P[m].evaluate(tx); + + for (int n = 0; n <= (ORDER - m); n++) { + luts[m][n].assign(x, y, pmx * P[n].evaluate(ty)); + } + } + } + } + } + // //-----------------// + // // checkOrthogonal // + // //-----------------// + // private static void checkOrthogonal () + // { + // for (int m = 0; m <= ORDER; m++) { + // for (int n = 0; n <= (ORDER - m); n++) { + // double val = 0; + // + // for (int x = 0; x < LUT_SIZE; x++) { + // double tx = (x - lutRadius) / (double) lutRadius; + // + // val += ((P[m].evaluate(tx) * P[n].evaluate(tx)) / lutRadius); + // } + // + // double exp = (m == n) ? (2.0 / ((2 * m) + 1)) : 0; + // + // System.out.println( + // "m:" + m + " n:" + n + " exp:" + + // String.format("%5.2f", exp) + " val:" + + // String.format("%5.2f", Math.abs(val))); + // } + // } + // } +} diff --git a/src/main/omr/moments/BasicLegendreMoments.java b/src/main/omr/moments/BasicLegendreMoments.java new file mode 100644 index 0000000..c8fce1a --- /dev/null +++ b/src/main/omr/moments/BasicLegendreMoments.java @@ -0,0 +1,111 @@ +//----------------------------------------------------------------------------// +// // +// B a s i c L e g e n d r e M o m e n t s // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.moments; + +import java.util.Locale; + +/** + * Class {@code BasicLegendreMoments} implements a descriptor for + * orthogonal Legendre moments. + * + * @author Hervé Bitteur + */ +public class BasicLegendreMoments + implements LegendreMoments +{ + //~ Instance fields -------------------------------------------------------- + + /** Resulting moments. */ + protected double[][] moments = new double[ORDER + 1][ORDER + 1]; + + //~ Constructors ----------------------------------------------------------- + //----------------------// + // BasicLegendreMoments // + //----------------------// + /** + * Creates a new BasicLegendreMoments object. + */ + public BasicLegendreMoments () + { + } + + //~ Methods ---------------------------------------------------------------- + //------------// + // distanceTo // + //------------// + @Override + public double distanceTo (LegendreMoments that) + { + double distance = 0; + + for (int m = 0; m <= ORDER; m++) { + for (int n = 0; n <= ORDER; n++) { + if ((m + n) <= ORDER) { + distance += Math.abs( + that.getMoment(m, n) - getMoment(m, n)); + } + } + } + + return distance; + } + + //-----------// + // getMoment // + //-----------// + @Override + public double getMoment (int m, + int n) + { + return moments[m][n]; + } + + //-----------// + // setMoment // + //-----------// + @Override + public void setMoment (int m, + int n, + double value) + { + moments[m][n] = value; + } + + //----------// + // toString // + //----------// + @Override + public String toString () + { + StringBuilder sb = new StringBuilder("{"); + + for (int m = 0; m <= ORDER; m++) { + for (int n = 0; n <= ORDER; n++) { + if ((m + n) <= ORDER) { + if (sb.length() > 1) { + sb.append(" "); + } + + sb.append( + String.format( + Locale.US, + "%04.0f", + 1000 * getMoment(m, n))); + } + } + } + + sb.append("}"); + + return sb.toString(); + } +} diff --git a/src/main/omr/moments/GeometricMoments.java b/src/main/omr/moments/GeometricMoments.java new file mode 100644 index 0000000..3104984 --- /dev/null +++ b/src/main/omr/moments/GeometricMoments.java @@ -0,0 +1,400 @@ +//----------------------------------------------------------------------------// +// // +// G e o m e t r i c M o m e n t s // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.moments; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.awt.Point; + +/** + * Class {@code GeometricMoments} encapsulates the set of all + * geometric moments that characterize an image. + * + * We use only central moments (invariant Hu moments are disabled by default). + * + * @author Hervé Bitteur + */ +public class GeometricMoments +{ + //~ Static fields/initializers --------------------------------------------- + + /** Usual logger utility */ + private static final Logger logger = LoggerFactory.getLogger( + GeometricMoments.class); + + // Hu coefficients are optional + public static final boolean useHuCoefficients = false; + + /** Number of features handled: {@value} */ + public static final int size = 12 + (useHuCoefficients ? 7 : 0); + + /** Labels for better display */ + private static final String[] labels = { + /** + * Unit-normalized stuff + */ + "weight", // 0 + "width", // 1 + "height", // 2 + /** + * Mass-normalized central moments + */ + "n20", // 3 + "n11", // 4 + "n02", // 5 + "n30", // 6 + "n21", // 7 + "n12", // 8 + "n03", // 9 + /** + * Mass center + */ + "xBar", // 10 + "yBar", // 11 + /** + * Hu coefficients, if any + */ + "h1", // 12 + "h2", // 13 + "h3", // 14 + "h4", // 15 + "h5", // 16 + "h6", // 17 + "h7", // 18 + }; + + //~ Instance fields -------------------------------------------------------- + /** The various moments, implemented as an array of double's. */ + private final Double[] k = new Double[size]; + + //~ Constructors ----------------------------------------------------------- + //------------------// + // GeometricMoments // + //------------------// + /** + * Creates a new GeometricMoments object. + * + * @param that the other GeometricMoments to clone + */ + public GeometricMoments (GeometricMoments that) + { + System.arraycopy(that.k, 0, this.k, 0, size); + } + + //------------------// + // GeometricMoments // + //------------------// + /** + * Compute the moments for a set of points whose x and y + * coordinates are provided, all values being normalized by the + * provided unit value. + * + * @param xx the array of abscissa values + * @param yy the array of ordinate values + * @param dim the number of points + * @param unit the length (number of pixels) of normalizing unit + */ + public GeometricMoments (int[] xx, + int[] yy, + int dim, + int unit) + { + // Safety check + if (unit == 0) { + throw new IllegalArgumentException("Zero-valued unit"); + } + + int xMin = Integer.MAX_VALUE; + int xMax = Integer.MIN_VALUE; + int yMin = Integer.MAX_VALUE; + int yMax = Integer.MIN_VALUE; + + // Normalized GeometricMoments + double n00 = (double) dim / (double) (unit * unit); + double n01 = 0d; + double n02 = 0d; + double n03 = 0d; + double n10 = 0d; + double n11 = 0d; + double n12 = 0d; + double n20 = 0d; + double n21 = 0d; + double n30 = 0d; + + // Total weight + double w = dim; // For p+q == 0 + double w2 = w * w; // For p+q == 2 + double w3 = Math.sqrt(w * w * w * w * w); // For p+q == 3 + + // Mean x & y, width & height + for (int i = dim - 1; i >= 0; i--) { + int x = xx[i]; + n10 += x; + + if (x < xMin) { + xMin = x; + } + + if (x > xMax) { + xMax = x; + } + + int y = yy[i]; + n01 += y; + + if (y < yMin) { + yMin = y; + } + + if (y > yMax) { + yMax = y; + } + } + + n10 /= dim; + n01 /= dim; + + for (int i = dim - 1; i >= 0; i--) { + // Coordinates centered around center of mass + double x = xx[i] - n10; + double y = yy[i] - n01; + n11 += (x * y); + n12 += (x * y * y); + n21 += (x * x * y); + n20 += (x * x); + n02 += (y * y); + n30 += (x * x * x); + n03 += (y * y * y); + } + + // Normalize + // + // p + q = 2 + n11 /= w2; + n20 /= w2; + n02 /= w2; + // + // p + q = 3 + n12 /= w3; + n21 /= w3; + n30 /= w3; + n03 /= w3; + + // Unit-based weight, width and height + k[0] = n00; // Unit-based Weight + k[1] = (double) (xMax - xMin + 1) / unit; // Unit-based Width + k[2] = (double) (yMax - yMin + 1) / unit; // Unit-based Height + + // Non-orthogonal central moments + // (invariant to translation & scaling) + k[3] = n20; // X absolute eccentricity + k[4] = n11; // XY covariance + k[5] = n02; // Y absolute eccentricity + k[6] = n30; // X signed eccentricity + k[7] = n21; // V vs. ^ + k[8] = n12; // > vs. < + k[9] = n03; // Y signed eccentricity + + // Mass center + k[10] = n10; // xBar + k[11] = n01; // yBar + + if (useHuCoefficients) { + // Orthogonals moments (Hu set) + // (Invariant to translation / scaling / rotation) + int i = 12; + k[i++] = n20 + n02; + // + k[i++] = ((n20 - n02) * (n20 - n02)) + (4 * n11 * n11); + // + k[i++] = ((n30 - (3 * n12)) * (n30 - (3 * n12))) + + ((n03 - (3 * n21)) * (n03 - (3 * n21))); + // + k[i++] = ((n30 + n12) * (n30 + n12)) + ((n03 + n21) * (n03 + n21)); + // + k[i++] = ((n30 - (3 * n12)) * (n30 + n12) * (((n30 + n12) * (n30 + + n12)) + - (3 * (n21 + n03) * (n21 + + n03)))) + + ((n03 - (3 * n21)) * (n03 + n21) * (((n03 + n21) * (n03 + + n21)) + - (3 * (n12 + n30) * (n12 + + n30)))); + // + k[i++] = ((n20 - n02) * (((n30 + n12) * (n30 + n12)) + - ((n03 + n21) * (n03 + n21)))) + + (4 * n11 * (n30 + n12) * (n03 + n21)); + // + k[i++] = (((3 * n21) - n03) * (n30 + n12) * (((n30 + n12) * (n30 + + n12)) + - (3 * (n21 + n03) * (n21 + + n03)))) + - (((3 * n12) - n30) * (n03 + n21) * (((n03 + n21) * (n03 + + n21)) + - (3 * (n12 + n30) * (n12 + + n30)))); + } + } + + //------------------// + // GeometricMoments // + //------------------// + /** + * No-arg constructor, needed for XML binder. + */ + public GeometricMoments () + { + } + + //~ Methods ---------------------------------------------------------------- + //----------// + // getLabel // + //----------// + /** + * Report the label related to moment at specified index. + * + * @param index the moment index + * @return the related index + */ + public static String getLabel (int index) + { + return labels[index]; + } + + //-------------// + // getCentroid // + //-------------// + /** + * Report the mass center of the glyph. + * + * @return the centroid + */ + public Point getCentroid () + { + return new Point((int) Math.rint(k[10]), (int) Math.rint(k[11])); + } + + //-----------// + // getHeight // + //-----------// + /** + * Report the height of the glyph, normalized by unit. + * + * @return the normalized height + */ + public Double getHeight () + { + return k[2]; + } + + //--------// + // getN12 // + //--------// + /** + * Report the n11 moment (which relates to xy covariance). + * + * @return the n11 moment + */ + public Double getN11 () + { + return k[4]; + } + + //--------// + // getN12 // + //--------// + /** + * Report the n12 moment (which relates to xy2: > vs <). + * + * @return the n12 moment + */ + public Double getN12 () + { + return k[8]; + } + + //--------// + // getN21 // + //--------// + /** + * Report the n21 moment (which relates to x2y: V vs ^). + * + * @return the n21 moment + */ + public Double getN21 () + { + return k[7]; + } + + //-----------// + // getValues // + //-----------// + /** + * Report the array of moment values. + * + * @return the moment values + */ + public Double[] getValues () + { + return k; + } + + //-----------// + // getWeight // + //-----------// + /** + * Report the total weight of the glyph, normalized by unit**2. + * + * @return the normalized weight + */ + public Double getWeight () + { + return k[0]; + } + + //----------// + // getWidth // + //----------// + /** + * Report the width of the glyph, normalized by unit. + * + * @return the normalized width + */ + public Double getWidth () + { + return k[1]; + } + + //----------// + // toString // + //----------// + @Override + public String toString () + { + StringBuilder sb = new StringBuilder(); + sb.append("{Moments"); + + for (int i = 0; i < k.length; i++) { + sb.append(" ") + .append(i) + .append("/") + .append(labels[i]) + .append("=") + .append(String.format("%g", k[i])); + } + + sb.append("}"); + + return sb.toString(); + } +} diff --git a/src/main/omr/moments/LUT.java b/src/main/omr/moments/LUT.java new file mode 100644 index 0000000..ef2b9f4 --- /dev/null +++ b/src/main/omr/moments/LUT.java @@ -0,0 +1,77 @@ +//----------------------------------------------------------------------------// +// // +// L U T // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.moments; + +/** + * Interface {@code LUT} defines a lookup table. + * + * @author Hervé Bitteur + */ +public interface LUT +{ + //~ Methods ---------------------------------------------------------------- + + /** + * Set the value for integer coordinates (x,y). + * + * @param x integer abscissa + * @param y integer ordinate + * @param value the known value for (x,y) point + */ + void assign (int x, + int y, + double value); + + /** + * Check whether the provided radius lies within the LUT. + * + * @param radius the radius to check + * @return true if OK + */ + boolean contains (double radius); + + /** + * Check whether the provided coordinates lies within the LUT + * range ([0, SIZE[). + * + * @param x provided abscissa + * @param y provided ordinate + * @return true if OK + */ + boolean contains (double x, + double y); + + /** + * Report the LUT radius (LUT implements (-radius,+radius). + * + * @return the defined radius + */ + int getRadius (); + + /** + * Report the LUT size (typically 2*radius +1). + * + * @return the LUT size + */ + int getSize (); + + /** + * Report the value for precise point (px,yy) by interpolation + * of values defined for integer coordinates. + * + * @param px precise abscissa + * @param py precise ordinate + * @return the interpolated value + */ + double interpolate (double px, + double py); +} diff --git a/src/main/omr/moments/LegendreMoments.java b/src/main/omr/moments/LegendreMoments.java new file mode 100644 index 0000000..60e5cce --- /dev/null +++ b/src/main/omr/moments/LegendreMoments.java @@ -0,0 +1,28 @@ +//----------------------------------------------------------------------------// +// // +// L e g e n d r e M o m e n t s // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.moments; + +/** + * Class {@code LegendreMoments} defines a descriptor for orthogonal + * Legendre moments. + * + * @author Hervé Bitteur + */ +public interface LegendreMoments + extends OrthogonalMoments +{ + //~ Static fields/initializers --------------------------------------------- + + /** Chosen polynomial order. */ + public static final int ORDER = 10; + +} diff --git a/src/main/omr/moments/MomentsExtractor.java b/src/main/omr/moments/MomentsExtractor.java new file mode 100644 index 0000000..7c8e47f --- /dev/null +++ b/src/main/omr/moments/MomentsExtractor.java @@ -0,0 +1,54 @@ +//----------------------------------------------------------------------------// +// // +// M o m e n t s E x t r a c t o r // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.moments; + +import java.awt.image.WritableRaster; + +/** + * Interface {@code MomentsExtractor} is a general definition for an + * extractor of {@link OrthogonalMoments}. + * + * @param the descriptor type + * + * @author Hervé Bitteur + */ +public interface MomentsExtractor> +{ + //~ Methods ---------------------------------------------------------------- + + /** + * Extract information from provided foreground points and save + * the results in the target descriptor. + * + * @param xx the array of abscissa values + * @param yy the array of ordinate values + * @param mass the number of points + */ + void extract (int[] xx, + int[] yy, + int mass); + + /** + * Reconstruct an image from the shape features. + * + * @param raster the (square) raster to populate + */ + void reconstruct (WritableRaster raster); + + /** + * Assign a target descriptor of this extractor, to receive + * extraction results. + * + * @param descriptor the target descriptor + */ + void setDescriptor (D descriptor); +} diff --git a/src/main/omr/moments/OrthogonalMoments.java b/src/main/omr/moments/OrthogonalMoments.java new file mode 100644 index 0000000..0ab161a --- /dev/null +++ b/src/main/omr/moments/OrthogonalMoments.java @@ -0,0 +1,63 @@ +//----------------------------------------------------------------------------// +// // +// O r t h o g o n a l M o m e n t s // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.moments; + +/** + * Interface {@code OrthogonalMoments} is a general definition for a + * descriptor of orthogonal moments. + * + * @param the descriptor type + * + * @author Hervé Bitteur + */ +public interface OrthogonalMoments> +{ + //~ Methods ---------------------------------------------------------------- + + /** + * Report the distance to another descriptor instance. + * + * @param that the other instance + * @return the measured distance + */ + double distanceTo (D that); + + /** + * Report the moment for m and n orders. + * + * @param m m order + * @param n n order + * @return moments(m, n) + */ + double getMoment (int m, + int n); + + /** + * Assign the moment for m and n orders. + * + * @param m m order + * @param n n order + * @param value the moment value + */ + void setMoment (int m, + int n, + double value); + // /** + // * Report a label for the (m,n) moment. + // * @param m m order + // * @param n n order + // * @return label for (m, n) + // */ + // String getLabel (int m, + // int n); + // +} diff --git a/src/main/omr/moments/QuantizedARTMoments.java b/src/main/omr/moments/QuantizedARTMoments.java new file mode 100644 index 0000000..924543f --- /dev/null +++ b/src/main/omr/moments/QuantizedARTMoments.java @@ -0,0 +1,212 @@ +//----------------------------------------------------------------------------// +// // +// Q u a n t i z e d A R T M o m e n t s // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.moments; + +import java.util.Arrays; + +/** + * Class {@code QuantizedARTMoments} handles a quantized region-based + * shape descriptor. + * + * See MPEG-7 Experimentation Model for the original C++ code + * + * @author Hervé Bitteur + */ +public class QuantizedARTMoments + implements ARTMoments +{ + //~ Static fields/initializers --------------------------------------------- + + // Quantization table (17 cells) + private static double[] quantTable = { + 0.000000000, 0.003585473, + 0.007418411, 0.011535520, + 0.015982337, 0.020816302, + 0.026111312, 0.031964674, + 0.038508176, 0.045926586, + 0.054490513, 0.064619488, + 0.077016351, 0.092998687, + 0.115524524, 0.154032694, + 1.000000000 + }; + + // Inverse quantization table (16 cells) + private static double[] iQuantTable = { + 0.001763817, 0.005468893, + 0.009438835, 0.013714449, + 0.018346760, 0.023400748, + 0.028960940, 0.035140141, + 0.042093649, 0.050043696, + 0.059324478, 0.070472849, + 0.084434761, 0.103127662, + 0.131506859, 0.192540857 + }; + + //~ Instance fields -------------------------------------------------------- + // double QuantizedARTMoments::QuantTable[17] = {0.000000000, 0.001898192, 0.003927394, 0.006107040, 0.008461237, 0.011020396, 0.013823636, 0.016922475, 0.020386682, 0.024314076, 0.028847919, 0.034210318, 0.040773364, 0.049234601, 0.061160045, 0.081546727, 1.0}; + // double QuantizedARTMoments::IQuantTable[16] = {0.000933785, 0.002895296, 0.004997030, 0.007260591, 0.009712991, 0.012388631, 0.015332262, 0.018603605, 0.022284874, 0.026493722, 0.031407077, 0.037309157, 0.044700757, 0.054597000, 0.069621283, 0.101933409}; + // + /** Actual values */ + private final short[][] values = new short[ANGULAR][RADIAL]; + + //~ Constructors ----------------------------------------------------------- + //---------------------// + // QuantizedARTMoments // + //---------------------// + /** + * Creates a new QuantizedARTMoments object. + */ + public QuantizedARTMoments () + { + } + + //~ Methods ---------------------------------------------------------------- + //-----------// + // getModule // + //-----------// + @Override + public final double getModule (int p, + int r) + { + return iQuantTable[values[p][r]]; + } + + //-----------// + // setModule // + //-----------// + @Override + public final void setModule (int p, + int r, + double value) + { + /* + * index of the search key, if it is contained in the array; otherwise, + * (-(insertion point) - 1). The insertion point is defined as the point + * at which the key would be inserted into the array: the index of the + * first element greater than the key, or a.length if all elements in + * the array are less than the specified key. Note that this guarantees + * that the return value will be >= 0 if and only if the key is found. + */ + int idx = Arrays.binarySearch(quantTable, value); + + if (idx < 0) { + idx = -idx - 2; + } + + values[p][r] = (short) idx; + } + + //------------// + // distanceTo // + //------------// + @Override + public double distanceTo (ARTMoments that) + { + double distance = 0; + + for (int p = 0; p < ANGULAR; p++) { + for (int r = 0; r < RADIAL; r++) { + if ((p != 0) || (r != 0)) { + distance += Math.abs( + that.getModule(p, r) - getModule(p, r)); + } + } + } + + return distance; + } + + @Override + public double getArgument (int p, + int r) + { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public double getImag (int p, + int r) + { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public double getMoment (int m, + int n) + { + return getModule(m, n); + } + + @Override + public double getReal (int p, + int r) + { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public void setArgument (int p, + int r, + double value) + { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public void setImag (int p, + int r, + double value) + { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public void setMoment (int m, + int n, + double value) + { + setModule(m, n, value); + } + + @Override + public void setReal (int p, + int r, + double value) + { + throw new UnsupportedOperationException("Not supported yet."); + } + + //----------// + // toString // + //----------// + @Override + public String toString () + { + StringBuilder sb = new StringBuilder("{"); + + for (int p = 0; p < ANGULAR; p++) { + for (int r = 0; r < RADIAL; r++) { + if ((p != 0) || (r != 0)) { + if (sb.length() > 1) { + sb.append(" "); + } + + sb.append(values[p][r]); + } + } + } + + sb.append("}"); + + return sb.toString(); + } +} diff --git a/src/main/omr/moments/doc-files/moments.uxf b/src/main/omr/moments/doc-files/moments.uxf new file mode 100644 index 0000000..ba847c1 --- /dev/null +++ b/src/main/omr/moments/doc-files/moments.uxf @@ -0,0 +1,301 @@ + + + 10 + + com.umlet.element.Class + + 310 + 20 + 170 + 100 + + <<interface>> +*/OrthogonalMoments/* +bg=#ffffaa +-- +setMoment() +getMoment() +distanceTo() + + + + + com.umlet.element.Class + + 220 + 190 + 160 + 60 + + <<interface>> +*/LegendreMoments/* +bg=#ffffaa +-- +ORDER + + + + + com.umlet.element.Class + + 410 + 190 + 170 + 80 + + <<interface>> +*/ARTMoments/* +bg=#ffffaa +-- +ANGULAR +RADIAL + + + + + com.umlet.element.Relation + + 410 + 90 + 50 + 120 + + lt=<<- + 30;30;30;100 + + + com.umlet.element.Relation + + 320 + 90 + 50 + 120 + + lt=<<- + 30;30;30;100 + + + com.umlet.element.Class + + 410 + 330 + 140 + 30 + + *BasicARTMoments* + + + + + com.umlet.element.Class + + 520 + 370 + 170 + 30 + + *QuantizedARTMoments* + + + + + com.umlet.element.Relation + + 530 + 240 + 50 + 150 + + lt=<. + 30;30;30;130 + + + com.umlet.element.Relation + + 460 + 240 + 50 + 110 + + lt=<. + 30;30;30;90 + + + com.umlet.element.Class + + 200 + 330 + 180 + 30 + + *BasicLegendreMoments* + + + + + com.umlet.element.Relation + + 270 + 220 + 50 + 130 + + lt=<. + 30;30;30;110 + + + com.umlet.element.Class + + 20 + 330 + 150 + 30 + + *GeometricMoments* + + + + + com.umlet.element.Class + + 320 + 490 + 150 + 110 + + <<interface>> +*/MomentsExtractor/* +bg=#ffffaa +<Descriptor> +-- +extract() +reconstruct() +setDescriptor() + + + + + com.umlet.element.Class + + 320 + 640 + 150 + 190 + + */AbstractExtractor/* +<Descriptor> +-- +xx +yy +mass +center +radius +descriptor +-- +extract() +setDescriptor() + + + + + com.umlet.element.Relation + + 370 + 570 + 50 + 90 + + lt=<. + 30;30;30;70 + + + com.umlet.element.Class + + 410 + 880 + 150 + 50 + + *BasicARTExtractor* +-- +extractMoments() + + + + + com.umlet.element.Class + + 200 + 880 + 180 + 70 + + *BasicLegendreExtractor* +-- +extractMoments() +reconstruct() + + + + + com.umlet.element.Relation + + 400 + 800 + 50 + 100 + + lt=<<- + 30;30;30;80 + + + com.umlet.element.Relation + + 330 + 800 + 50 + 100 + + lt=<<- + 30;30;30;80 + + + com.umlet.element.Class + + 580 + 490 + 100 + 80 + + <<interface>> +*/LUT/* +bg=#ffffaa +-- +assign() +interpolate() + + + + com.umlet.element.Class + + 580 + 620 + 100 + 30 + + *BasicLUT* + + + + + com.umlet.element.Relation + + 600 + 540 + 50 + 100 + + lt=<. + 30;30;30;80 + + diff --git a/src/main/omr/moments/package.html b/src/main/omr/moments/package.html new file mode 100644 index 0000000..f1afc43 --- /dev/null +++ b/src/main/omr/moments/package.html @@ -0,0 +1,18 @@ + + + + + Package omr.moments + + + + +
+ This package is dedicated to the extraction and use of + mathematics moments for image description and recognition. + +

Synopsis:

+ +
+ + diff --git a/src/main/omr/package.html b/src/main/omr/package.html new file mode 100644 index 0000000..b7795f3 --- /dev/null +++ b/src/main/omr/package.html @@ -0,0 +1,16 @@ + + + + + Package omr + + + +

+ Root package for the OMR application, providing application-wide + entities. All other packages are sub-packages of this one. +

+ + + diff --git a/src/main/omr/plugin/JavascriptUnavailableException.java b/src/main/omr/plugin/JavascriptUnavailableException.java new file mode 100644 index 0000000..71c8579 --- /dev/null +++ b/src/main/omr/plugin/JavascriptUnavailableException.java @@ -0,0 +1,23 @@ +//----------------------------------------------------------------------------// +// // +// J a v a s c r i p t U n a v a i l a b l e E x c e p t i o n // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.plugin; + +/** + * Class {code JavascriptUnavailableException} is raised when no + * JavaScript engine can be found. + * + * @author Etiolles + */ +public class JavascriptUnavailableException + extends Exception +{ +} diff --git a/src/main/omr/plugin/Plugin.java b/src/main/omr/plugin/Plugin.java new file mode 100644 index 0000000..3824cf5 --- /dev/null +++ b/src/main/omr/plugin/Plugin.java @@ -0,0 +1,317 @@ +//----------------------------------------------------------------------------// +// // +// P l u g i n // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.plugin; + +import omr.score.Score; + +import omr.step.Stepping; +import omr.step.Steps; + +import omr.util.BasicTask; +import omr.util.FileUtil; + +import org.jdesktop.application.Task; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.*; +import java.util.List; + +import javax.script.Invocable; +import javax.script.ScriptEngine; +import javax.script.ScriptEngineManager; +import javax.script.ScriptException; +import omr.WellKnowns; + +/** + * Class {@code Plugin} describes a plugin instance, encapsulating the + * relationship with the underlying javascript file. + * + *

A plugin is meant to describe the connection between Audiveris and an + * external program, which will consume the MusicXML file exported by + * Audiveris.

+ * + *

A plugin is a javascript file, meant to export: + *

+ *
pluginTitle
+ *
(string) The title to appear in Plugins pull-down menu
+ *
pluginTip
+ *
(string) A description text to appear as a user tip in Plugins menu
+ *
pluginCli
+ *
(function) A javascript function which returns the precise list of + * arguments used when calling the external program. Note that the actual call + * is not made by the javascript code, but by Audiveris itself for an easier + * handling of input and output streams.
+ *
+ * + * @author Hervé Bitteur + */ +public class Plugin +{ + //~ Static fields/initializers --------------------------------------------- + + /** Usual logger utility */ + private static final Logger logger = LoggerFactory.getLogger(Plugin.class); + + //~ Instance fields -------------------------------------------------------- + // + /** Related javascript file. */ + private final File file; + + /** Related engine. */ + private ScriptEngine engine; + + /** Plugin title. */ + private String title; + + /** Description used for tool tip. */ + private String tip; + + //~ Constructors ----------------------------------------------------------- + //--------// + // Plugin // + //--------// + /** + * Creates a new Plugin object. + * + * @param file related javascript file + */ + public Plugin (File file) + throws JavascriptUnavailableException + { + this.file = file; + + evaluateScript(); + + logger.debug("Created {}", this); + } + + //~ Methods ---------------------------------------------------------------- + //----------------// + // getDescription // + //----------------// + /** + * Report a descriptive sentence for this plugin. + * + * @return a sentence meant for tool tip + */ + public String getDescription () + { + if (tip != null) { + return tip; + } else { + // Default value + return getId(); + } + } + + //-------// + // getId // + //-------// + /** + * Report a unique ID for this plugin. + * + * @return plugin unique ID + */ + public String getId () + { + return FileUtil.getNameSansExtension(file); + } + + //---------// + // getTask // + //---------// + /** + * Report the asynchronous plugin task on provided score. + * + * @param score the score to process through this plugin + */ + public Task getTask (Score score) + { + return new PluginTask(score); + } + + //----------// + // getTitle // + //----------// + /** + * Report a title meant for user interface. + * + * @return a title for this plugin + */ + public String getTitle () + { + if (title != null) { + return title; + } else { + return getId(); + } + } + + //-----------// + // runPlugin // + //-----------// + public Void runPlugin (Score score) + { + // Make sure we have the export file + Stepping.ensureScoreStep(Steps.valueOf(Steps.EXPORT), score); + + final File exportFile = score.getExportFile(); + + if (exportFile == null) { + logger.warn("Could not get export file"); + + return null; + } + + // Retrieve proper sequence of command items + List args; + + try { + logger.debug("{} doInBackground on {}", Plugin.this, exportFile); + + Invocable inv = (Invocable) engine; + Object obj = inv.invokeFunction( + "pluginCli", + exportFile.getAbsolutePath()); + + if (obj instanceof List) { + args = (List) obj; // Unchecked by compiler + logger.debug("{} command args: {}", this, args); + } else { + return null; + } + } catch (ScriptException | NoSuchMethodException ex) { + logger.warn(this + " error invoking javascript", ex); + + return null; + } + + // Spawn the command + logger.info("Launching {} on {}", getTitle(), score.getRadix()); + + ProcessBuilder pb = new ProcessBuilder(args); + pb = pb.redirectErrorStream(true); + + try { + Process process = pb.start(); + InputStream is = process.getInputStream(); + InputStreamReader isr = new InputStreamReader(is, + WellKnowns.FILE_ENCODING); + BufferedReader br = new BufferedReader(isr); + + // Consume process output + String line; + + while ((line = br.readLine()) != null) { + logger.debug(line); + } + + // Wait to get exit value + try { + int exitValue = process.waitFor(); + + if (exitValue != 0) { + logger.warn("{} exited with value {}", + Plugin.this, exitValue); + } else { + logger.debug("{} exit value is {}", + Plugin.this, exitValue); + } + } catch (InterruptedException ex) { + ex.printStackTrace(); + } + } catch (IOException ex) { + logger.warn(Plugin.this + " error launching editor", ex); + } + + return null; + } + + //----------// + // toString // + //----------// + @Override + public String toString () + { + StringBuilder sb = new StringBuilder("{"); + sb.append(getClass().getSimpleName()); + + sb.append(" ").append(getId()); + + sb.append("}"); + + return sb.toString(); + } + + //----------------// + // evaluateScript // + //----------------// + /** + * Evaluate the plugin script to get precise information built. + */ + private void evaluateScript () + throws JavascriptUnavailableException + { + ScriptEngineManager mgr = new ScriptEngineManager(); + engine = mgr.getEngineByName("JavaScript"); + + if (engine != null) { + try { + InputStream is = new FileInputStream(file); + Reader reader = new InputStreamReader(is, WellKnowns.FILE_ENCODING); + engine.eval(reader); + + // Retrieve information from script + title = (String) engine.get("pluginTitle"); + tip = (String) engine.get("pluginTip"); + } catch (FileNotFoundException | UnsupportedEncodingException | + ScriptException ex) { + logger.warn(this + " error", ex); + } + } else { + throw new JavascriptUnavailableException(); + } + } + + //~ Inner Classes ---------------------------------------------------------- + //------------// + // PluginTask // + //------------// + /** + * Handles the processing defined by the underlying javascript. + * The lifecycle of this instance is limited to the duration of the task. + */ + private class PluginTask + extends BasicTask + { + //~ Instance fields ---------------------------------------------------- + + private final Score score; + + //~ Constructors ------------------------------------------------------- + public PluginTask (Score score) + { + this.score = score; + } + + //~ Methods ------------------------------------------------------------ + @Override + @SuppressWarnings("unchecked") + protected Void doInBackground () + throws InterruptedException + { + return Plugin.this.runPlugin(score); + } + } +} diff --git a/src/main/omr/plugin/PluginAction.java b/src/main/omr/plugin/PluginAction.java new file mode 100644 index 0000000..b7516cb --- /dev/null +++ b/src/main/omr/plugin/PluginAction.java @@ -0,0 +1,73 @@ +//----------------------------------------------------------------------------// +// // +// P l u g i n A c t i o n // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.plugin; + +import omr.score.Score; +import omr.score.ui.ScoreController; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.awt.event.ActionEvent; + +import javax.swing.AbstractAction; + +/** + * Class {@code PluginAction} implements the concrete user action + * related to a registered plugin. + * + * @author Hervé Bitteur + */ +class PluginAction + extends AbstractAction +{ + //~ Static fields/initializers --------------------------------------------- + + /** Usual logger utility */ + private static final Logger logger = LoggerFactory.getLogger( + PluginAction.class); + + //~ Instance fields -------------------------------------------------------- + /** The related plugin */ + private final Plugin plugin; + + //~ Constructors ----------------------------------------------------------- + //--------------// + // PluginAction // + //--------------// + /** + * Creates a new PluginAction object. + * + * @param plugin the underlying scripting plugin + */ + public PluginAction (Plugin plugin) + { + super(plugin.getTitle()); + this.plugin = plugin; + putValue(SHORT_DESCRIPTION, plugin.getDescription()); + } + + //~ Methods ---------------------------------------------------------------- + //-----------------// + // actionPerformed // + //-----------------// + @Override + public void actionPerformed (ActionEvent e) + { + final Score score = ScoreController.getCurrentScore(); + + if (score != null) { + plugin.getTask(score) + .execute(); + } + } +} diff --git a/src/main/omr/plugin/PluginsManager.java b/src/main/omr/plugin/PluginsManager.java new file mode 100644 index 0000000..53dcf33 --- /dev/null +++ b/src/main/omr/plugin/PluginsManager.java @@ -0,0 +1,366 @@ +//----------------------------------------------------------------------------// +// // +// P l u g i n s M a n a g e r // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.plugin; + +import omr.WellKnowns; + +import omr.constant.Constant; +import omr.constant.ConstantSet; + +import omr.score.Score; +import omr.score.ui.ScoreController; +import omr.score.ui.ScoreDependent; + +import omr.sheet.ui.SheetsController; + +import omr.ui.util.SeparableMenu; + +import omr.util.FileUtil; +import omr.util.Param; + +import org.jdesktop.application.Action; +import org.jdesktop.application.Task; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.awt.event.ActionEvent; +import java.io.File; +import java.io.FileFilter; +import java.util.Collection; +import java.util.Map; +import java.util.TreeMap; + +import javax.swing.JMenu; +import javax.swing.JMenuItem; +import javax.swing.event.MenuEvent; +import javax.swing.event.MenuListener; +import omr.step.PluginStep; +import omr.step.Steps; + +/** + * Class {@code PluginsManager} handles the collection of application + * registered plugins. + * Each registered plugin is represented by a menu item. + * One of these plugins can be set as the default editor plugin and directly + * launched by the dedicated toolbar button. + * + *

Any file, with the ".js" extension, found in the + * plugins + * folder will lead to the creation of a corresponding Plugin instance.

+ * + * @author Hervé Bitteur + */ +public class PluginsManager + extends ScoreDependent +{ + //~ Static fields/initializers --------------------------------------------- + + /** Specific application parameters */ + private static final Constants constants = new Constants(); + + /** Usual logger utility */ + private static final Logger logger = LoggerFactory.getLogger(PluginsManager.class); + + /** Singleton. */ + private static PluginsManager INSTANCE; + + /** Filter for plugin script files. */ + private static final FileFilter pluginFilter = new FileFilter() + { + @Override + public boolean accept (File pathname) + { + // Check for proper extension + String ext = FileUtil.getExtension(pathname); + + if (ext.equalsIgnoreCase(".js")) { + return true; + } else { + return false; + } + } + }; + + /** Default plugin id. */ + public static final Param defaultPluginId = new Default(); + + //~ Instance fields -------------------------------------------------------- + // + /** The concrete UI menu. */ + private JMenu menu; + + /** The sorted collection of registered plugins: ID -> Plugin. */ + private final Map map = new TreeMap<>(); + + /** The default plugin. */ + private Plugin defaultPlugin; + + //~ Constructors ----------------------------------------------------------- + // + //----------------// + // PluginsManager // + //----------------// + /** + * Generates the menu to be inserted in the plugin menu hierarchy, + * based on the script files discovered in the plugin folder. + * + * @param menu the hosting menu, or null + */ + private PluginsManager () + { + // Browse the plugin folder for relevant scripts + File pluginDir = WellKnowns.PLUGINS_FOLDER; + + if (pluginDir.exists() && pluginDir.isDirectory()) { + for (File file : pluginDir.listFiles(pluginFilter)) { + try { + Plugin plugin = new Plugin(file); + map.put(plugin.getId(), plugin); + } catch (Exception ex) { + logger.warn("Could not process plugin file {} [{}]", + file, ex); + } + } + + // Default plugin, if any is defined + setDefaultPlugin(constants.defaultPlugin.getValue().trim()); + } + } + + //~ Methods ---------------------------------------------------------------- + //------------// + // getPlugins // + //------------// + /** + * Report the collection of plugins ids + * + * @return the various plugins ids + */ + public Collection getPluginIds () + { + return map.keySet(); + } + + //------------------// + // getDefaultPlugin // + //------------------// + /** + * Return the default plugin if any. + * + * @return the default plugin, or null if none is defined + */ + public Plugin getDefaultPlugin () + { + return defaultPlugin; + } + + //------------------// + // setDefaultPlugin // + //------------------// + /** + * Assign the default plugin. + */ + public final void setDefaultPlugin (String pluginId) + { + Plugin plugin = findDefaultPlugin(pluginId); + + if (!pluginId.isEmpty() && (plugin == null)) { + logger.warn("Could not find default plugin {}", pluginId); + } else { + setDefaultPlugin(plugin); + } + } + + //------------------// + // setDefaultPlugin // + //------------------// + /** + * Assign the default plugin. + */ + public final void setDefaultPlugin (Plugin defaultPlugin) + { + Plugin oldDefaultPlugin = this.defaultPlugin; + this.defaultPlugin = defaultPlugin; + + if (oldDefaultPlugin != null) { + PluginStep pluginStep = (PluginStep) Steps.valueOf(Steps.PLUGIN); + pluginStep.setPlugin(defaultPlugin); + } + } + + //-------------// + // getInstance // + //-------------// + /** + * Report the class singleton. + * + * @return the unique instance of this class + */ + public static synchronized PluginsManager getInstance () + { + if (INSTANCE == null) { + INSTANCE = new PluginsManager(); + } + + return INSTANCE; + } + + //---------// + // getMenu // + //---------// + /** + * Report the concrete UI menu of all plugins + * + * @param menu a preallocated menu instance, or null + * @return the populated menu entity + */ + public JMenu getMenu (JMenu menu) + { + if (menu == null) { + menu = new SeparableMenu(); + } + + for (Plugin plugin : map.values()) { + menu.add(new JMenuItem(new PluginAction(plugin))); + } + + // Listener to modify attributes on-the-fly + menu.addMenuListener(new MyMenuListener()); + + this.menu = menu; + + return menu; + } + + //--------------// + // invokeEditor // + //--------------// + /** + * Action to invoke the default score editor + * + * @param e the event that triggered this action + */ + @Action(enabledProperty = SCORE_AVAILABLE) + public Task invokeDefaultPlugin (ActionEvent e) + { + if (defaultPlugin == null) { + logger.warn("No default plugin defined"); + + return null; + } + + // Current score export file + final Score score = ScoreController.getCurrentScore(); + + if (score == null) { + return null; + } else { + return defaultPlugin.getTask(score); + } + } + + //-------------------// + // findDefaultPlugin // + //-------------------// + private Plugin findDefaultPlugin (String pluginId) + { + for (Plugin plugin : map.values()) { + if (plugin.getId().equalsIgnoreCase(pluginId)) { + return plugin; + } + } + + return null; + } + + //~ Inner Classes ---------------------------------------------------------- + //-----------// + // Constants // + //-----------// + private static final class Constants + extends ConstantSet + { + //~ Instance fields ---------------------------------------------------- + + Constant.String defaultPlugin = new Constant.String( + "", + "Name of default plugin"); + + } + +//----------------// +// MyMenuListener // +//----------------// + /** + * Class {@code MyMenuListener} is triggered when menu is entered. + * This is meant to enable menu items only when a sheet is selected. + */ + private class MyMenuListener + implements MenuListener + { + //~ Methods ------------------------------------------------------------ + + @Override + public void menuCanceled (MenuEvent e) + { + } + + @Override + public void menuDeselected (MenuEvent e) + { + } + + @Override + public void menuSelected (MenuEvent e) + { + boolean enabled = SheetsController.getCurrentSheet() != null; + + for (int i = 0; i < menu.getItemCount(); i++) { + JMenuItem menuItem = menu.getItem(i); + + // Beware of separators (for which returned menuItem is null) + if (menuItem != null) { + menuItem.setEnabled(enabled); + } + } + } + } + + //---------// + // Default // + //---------// + private static class Default + extends Param + { + + @Override + public String getSpecific () + { + return constants.defaultPlugin.getValue(); + } + + @Override + public boolean setSpecific (String specific) + { + if (!getSpecific().equals(specific)) { + constants.defaultPlugin.setValue(specific); + getInstance().setDefaultPlugin(specific); + logger.info("Default plugin is now ''{}''", specific); + + return true; + } + + return false; + } + } +} diff --git a/src/main/omr/plugin/package.html b/src/main/omr/plugin/package.html new file mode 100644 index 0000000..5fd4917 --- /dev/null +++ b/src/main/omr/plugin/package.html @@ -0,0 +1,14 @@ + + + + + Package plugin + + + + +

+ Package dedicated to handling of application plugins +

+ + diff --git a/src/main/omr/plugin/resources/PluginsManager.properties b/src/main/omr/plugin/resources/PluginsManager.properties new file mode 100644 index 0000000..40bfb87 --- /dev/null +++ b/src/main/omr/plugin/resources/PluginsManager.properties @@ -0,0 +1,13 @@ +# ---------------------------------------------------------------------------- # +# # +# P l u g i n s M a n a g e r . p r o p e r t i e s # +# # +# ---------------------------------------------------------------------------- # + +# This file gathers resources to be injected in the PluginsManager class +# +# This is the generic version + +invokeDefaultPlugin.Action.text = Default plugin +invokeDefaultPlugin.Action.shortDescription = Invoke default plugin +invokeDefaultPlugin.Action.icon = ${icons.root}/mimetypes/shellscript.png diff --git a/src/main/omr/plugin/resources/PluginsManager_fr.properties b/src/main/omr/plugin/resources/PluginsManager_fr.properties new file mode 100644 index 0000000..d0d2e52 --- /dev/null +++ b/src/main/omr/plugin/resources/PluginsManager_fr.properties @@ -0,0 +1,12 @@ +# ---------------------------------------------------------------------------- # +# # +# P l u g i n s M a n a g e r _ f r . p r o p e r t i e s # +# # +# ---------------------------------------------------------------------------- # + +# This file gathers resources to be injected in the PluginsManager class +# +# This is the FR version + +invokeDefaultPlugin.Action.text = Editeur par d\u00e9faut +invokeDefaultPlugin.Action.shortDescription = Lancement de l'\u00e9diteur par d\u00e9faut diff --git a/src/main/omr/run/AdaptiveDescriptor.java b/src/main/omr/run/AdaptiveDescriptor.java new file mode 100644 index 0000000..54316ac --- /dev/null +++ b/src/main/omr/run/AdaptiveDescriptor.java @@ -0,0 +1,168 @@ +//----------------------------------------------------------------------------// +// // +// A d a p t i v e D e s c r i p t o r // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.run; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.lang.reflect.Constructor; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlAttribute; +import javax.xml.bind.annotation.XmlRootElement; + +/** + * Class {@code AdaptiveDescriptor} describes an {@link AdaptiveFilter} + * + * @author Hervé Bitteur + */ +@XmlAccessorType(XmlAccessType.NONE) +@XmlRootElement(name = "adaptive-filter") +public class AdaptiveDescriptor + extends FilterDescriptor +{ + //~ Static fields/initializers --------------------------------------------- + + /** Usual logger utility */ + private static final Logger logger = LoggerFactory.getLogger( + AdaptiveDescriptor.class); + + //~ Instance fields -------------------------------------------------------- + // + /** Coefficient for mean. */ + @XmlAttribute(name = "mean-coeff") + public final double meanCoeff; + + /** Coefficient for standard deviation. */ + @XmlAttribute(name = "std-dev-coeff") + public final double stdDevCoeff; + + //~ Constructors ----------------------------------------------------------- + // + //--------------------// + // AdaptiveDescriptor // + //--------------------// + /** + * Creates a new AdaptiveDescriptor object. + * + * @param meanCoeff Coefficient for mean value + * @param stdDevCoeff Coefficient for standard deviation value + */ + public AdaptiveDescriptor (double meanCoeff, + double stdDevCoeff) + { + this.meanCoeff = meanCoeff; + this.stdDevCoeff = stdDevCoeff; + } + + //--------------------// + // AdaptiveDescriptor // No-arg constructor meant for JAXB + //--------------------// + private AdaptiveDescriptor () + { + meanCoeff = 0; + stdDevCoeff = 0; + } + + //~ Methods ---------------------------------------------------------------- + //--------// + // equals // + //--------// + @Override + public boolean equals (Object obj) + { + if ((obj instanceof AdaptiveDescriptor) && super.equals(obj)) { + AdaptiveDescriptor that = (AdaptiveDescriptor) obj; + + return (this.meanCoeff == that.meanCoeff) + && (this.stdDevCoeff == that.stdDevCoeff); + } + + return false; + } + + //------------// + // getDefault // + //------------// + public static AdaptiveDescriptor getDefault () + { + return new AdaptiveDescriptor( + AdaptiveFilter.getDefaultMeanCoeff(), + AdaptiveFilter.getDefaultStdDevCoeff()); + } + + //-----------// + // getFilter // + //-----------// + @Override + public PixelFilter getFilter (PixelSource source) + { + Class classe = AdaptiveFilter.getImplementationClass(); + + try { + Constructor cons = classe.getConstructor( + new Class[]{PixelSource.class, double.class, double.class}); + + return (PixelFilter) cons.newInstance( + source, + meanCoeff, + stdDevCoeff); + } catch (Exception ex) { + logger.error("Error on getFilter {}", ex); + + return null; + } + } + + // + //---------// + // getKind // + //---------// + @Override + public FilterKind getKind () + { + return FilterKind.ADAPTIVE; + } + + //----------// + // hashCode // + //----------// + @Override + public int hashCode () + { + int hash = 5; + hash = (97 * hash) + + (int) (Double.doubleToLongBits(this.meanCoeff) + ^ (Double.doubleToLongBits(this.meanCoeff) >>> 32)); + hash = (97 * hash) + + (int) (Double.doubleToLongBits(this.stdDevCoeff) + ^ (Double.doubleToLongBits(this.stdDevCoeff) >>> 32)); + + return hash; + } + + //-----------------// + // internalsString // + //-----------------// + @Override + protected String internalsString () + { + StringBuilder sb = new StringBuilder(super.internalsString()); + sb.append(" meanCoeff:") + .append(meanCoeff); + sb.append(" stdDevCoeff:") + .append(stdDevCoeff); + + return sb.toString(); + } +} diff --git a/src/main/omr/run/AdaptiveFilter.java b/src/main/omr/run/AdaptiveFilter.java new file mode 100644 index 0000000..407e4f4 --- /dev/null +++ b/src/main/omr/run/AdaptiveFilter.java @@ -0,0 +1,436 @@ +//----------------------------------------------------------------------------// +// // +// A d a p t i v e F i l t e r // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.run; + +import omr.constant.Constant; +import omr.constant.ConstantSet; + +import omr.math.Population; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Arrays; + +/** + * Class {@code AdaptiveFilter} is an abstract implementation of + * {@code PixelFilter} which provides foreground information based on + * mean value and standard deviation in pixel neighborhood. + * + *

See work of Sauvola et al. + * here. + * + *

The mean value and the standard deviation value are provided thanks to + * underlying integrals {@link Tile} instances. + * The precise tile size and behavior is the responsibility of subclasses of + * this class. + * + *

See work of Shafait et al. + * here. + * + *

+ * 0---------------------------------------------+---------------+
+ * |                                             |               |
+ * |                                             |               |
+ * |                                             |               |
+ * |                                            a|              b|
+ * +---------------------------------------------+---------------+
+ * |                                             |               |
+ * |                                             |               |
+ * |                                             |               |
+ * |                                             |               |
+ * |                                             |               |
+ * |                                            c|              d|
+ * +---------------------------------------------+---------------+
+ * 
+ * Key table features: + *
    + *
  • Assumption: The integral of any rectangle with origin at (0,0) is stored + * in the bottom right cell of the rectangle.
  • + * + *
  • As a consequence the integral of any rectangle, whatever its origin, + * can be simply computed as: + * a + d - b - c + *
  • + * + *
  • In particular if lower right rectangle is reduced to a single cell, then + * d = pixel_value + top + left - topLeft
    + * This property is used to incrementally populate the table.
  • + *
+ * + * @author ryo/twitter @xiaot_Tag + * @author Hervé Bitteur + */ +public class AdaptiveFilter + extends SourceWrapper + implements PixelFilter +{ + //~ Static fields/initializers --------------------------------------------- + + /** Specific application parameters */ + private static final Constants constants = new Constants(); + + /** Usual logger utility */ + private static final Logger logger = LoggerFactory.getLogger( + AdaptiveFilter.class); + + //~ Instance fields -------------------------------------------------------- + // + /** Default value for (half of) window size. */ + protected final int HALF_WINDOW_SIZE = constants.halfWindowSize.getValue(); + + /** Coefficient of mean value. */ + protected final double MEAN_COEFF; + + /** Coefficient of standard deviation. */ + protected final double STD_DEV_COEFF; + + /** Table for integrals of plain values. */ + protected Tile tile; + + /** Table for integrals of squared values. */ + protected Tile sqrTile; + + //~ Constructors ----------------------------------------------------------- + // + //----------------// + // AdaptiveFilter // + //----------------// + /** + * Create an adaptive wrapper on a pixel source. + * + * @param source the underlying source of raw pixels + */ + public AdaptiveFilter (PixelSource source, + double meanCoeff, + double stdDevCoeff) + { + super(source); + + this.MEAN_COEFF = meanCoeff; + this.STD_DEV_COEFF = stdDevCoeff; + } + + //~ Methods ---------------------------------------------------------------- + //------------// + // getContext // + //------------// + @Override + public Context getContext (int x, + int y) + { + final int imageWidth = source.getWidth(); + final int imageHeight = source.getHeight(); + + int xMin = Math.max(0, x - HALF_WINDOW_SIZE); + int xMax = Math.min(imageWidth - 1, x + HALF_WINDOW_SIZE); + + int yMin = Math.max(0, y - HALF_WINDOW_SIZE); + int yMax = Math.min(imageHeight - 1, y + HALF_WINDOW_SIZE); + + // Brute force retrieval + Population pop = new Population(); + + for (int ix = xMin; ix <= xMax; ix++) { + for (int iy = yMin; iy <= yMax; iy++) { + pop.includeValue(source.getPixel(ix, iy)); + } + } + + if (pop.getCardinality() > 0) { + double mean = pop.getMeanValue(); + double stdDev = pop.getStandardDeviation(); + double threshold = getThreshold(mean, stdDev); + + return new AdaptiveContext(mean, stdDev, threshold); + } else { + return null; + } + } + + //---------------------// + // getDefaultMeanCoeff // + //---------------------// + public static double getDefaultMeanCoeff () + { + return constants.meanCoeff.getValue(); + } + + //-----------------------// + // getDefaultStdDevCoeff // + //-----------------------// + public static double getDefaultStdDevCoeff () + { + return constants.stdDevCoeff.getValue(); + } + + // + // -------// + // isFore // + // -------// + @Override + public boolean isFore (int x, + int y) + { + double mean = tile.getMean(x, y); + double sqrMean = sqrTile.getMean(x, y); + double var = Math.abs(sqrMean - (mean * mean)); + double stdDev = Math.sqrt(var); + + double threshold = getThreshold(mean, stdDev); + + int pixValue = source.getPixel(x, y); + boolean isFore = pixValue <= threshold; + + return isFore; + } + + //---------------------// + // setDefaultMeanCoeff // + //---------------------// + public static void setDefaultMeanCoeff (double meanCoeff) + { + constants.meanCoeff.setValue(meanCoeff); + } + + //-----------------------// + // setDefaultStdDevCoeff // + //-----------------------// + public static void setDefaultStdDevCoeff (double stdDevCoeff) + { + constants.stdDevCoeff.setValue(stdDevCoeff); + } + + //------------------// + // getAdaptiveClass // + //------------------// + static Class getImplementationClass () + { + String name = constants.className.getValue(); + + try { + return Class.forName(name); + } catch (ClassNotFoundException ex) { + logger.error("Cannot find adaptive filter class " + name); + + return null; + } + } + + //--------------// + // getThreshold // + //--------------// + private double getThreshold (double mean, + double stdDev) + { + // This is the key formula + return (MEAN_COEFF * mean) + (STD_DEV_COEFF * stdDev); + } + + //~ Inner Classes ---------------------------------------------------------- + //-----------------// + // AdaptiveContext // + //-----------------// + public static class AdaptiveContext + extends Context + { + //~ Instance fields ---------------------------------------------------- + + /** Mean pixel value in the neighborhood. */ + public final double mean; + + /** Standard deviation of pixel values in the neighborhood. */ + public final double standardDeviation; + + //~ Constructors ------------------------------------------------------- + public AdaptiveContext (double mean, + double standardDeviation, + double threshold) + { + super(threshold); + this.mean = mean; + this.standardDeviation = standardDeviation; + } + } + + // + //------// + // Tile // + //------// + /** + * Handles a vertical tile of integrals. + */ + protected class Tile + { + //~ Instance fields ---------------------------------------------------- + + /** Width of the tile circular buffer. */ + protected final int TILE_WIDTH; + + /** Remember if we handle squared values or plain values. */ + protected final boolean squared; + + /** Height of the tile = height of the image. */ + protected final int height; + + /** Abscissa corresponding to the right side of the tile. */ + protected int xRight = -1; + + /** Circular buffer for integrals. */ + protected final long[][] sums; + + //~ Constructors ------------------------------------------------------- + /** + * Create a tile instance. + * + * @param tileWidth tile width + * @param height tile height = image height + * @param squared true for squared values, false for plain values + */ + public Tile (int tileWidth, + int height, + boolean squared) + { + this.TILE_WIDTH = tileWidth; + this.height = height; + this.squared = squared; + + // Allocate buffer of integrals + sums = new long[TILE_WIDTH][height]; + + // Initialize the "previous" column + Arrays.fill(sums[TILE_WIDTH - 1], 0); + } + + //~ Methods ------------------------------------------------------------ + /** + * Make sure that the sliding window is positioned around the + * provided location, and return mean data. + * + * @param x provided abscissa + * @param y provided ordinate + * @return the average value around the provided location + */ + public double getMean (int x, + int y) + { + // Compute actual borders of the window + final int imageWidth = getWidth(); + + int x1 = Math.max(-1, x - HALF_WINDOW_SIZE - 1); + int x2 = Math.min(imageWidth - 1, x + HALF_WINDOW_SIZE); + + int y1 = Math.max(-1, y - HALF_WINDOW_SIZE - 1); + int y2 = Math.min(height - 1, y + HALF_WINDOW_SIZE); + + // Make sure the tile is positioned correctly + shiftTile(x2); + + // Upper left + long a = ((x1 >= 0) && (y1 >= 0)) ? sums[x1 % TILE_WIDTH][y1] : 0; + + // Above + long b = (y1 >= 0) ? sums[x2 % TILE_WIDTH][y1] : 0; + + // Left + long c = (x1 >= 0) ? sums[x1 % TILE_WIDTH][y2] : 0; + + // Lower right + long d = sums[x2 % TILE_WIDTH][y2]; + + // Integral for window rectangle + double sum = (a + d) - b - c; + + // Area = number of values + int area = (y2 - y1) * (x2 - x1); + + // Return mean value + return sum / area; + } + + /** + * Populate the provided column with proper integrals, building + * on the content of previous column. + * + * @param x the column to populate + */ + protected void populateColumn (int x) + { + // Translate the absolute column to circular buffer column + final int tx = x % TILE_WIDTH; + final long[] column = sums[tx]; + + // The column to the left (modulo tile width) + final int prevTx = ((x + TILE_WIDTH) - 1) % TILE_WIDTH; + final long[] prevColumn = sums[prevTx]; + + long top = 0; + long topLeft = 0; + + for (int y = 0; y < height; y++) { + long left = prevColumn[y]; + + long pix = getPixel(x, y); + + if (squared) { + pix *= pix; + } + + long val = (pix + left + top) - topLeft; + column[y] = val; + + // For next iteration + top = val; + topLeft = left; + } + } + + /** + * Make sure the column at abscissa 'x2' lies within the tile. + * + * @param x2 the abscissa to check + */ + protected void shiftTile (int x2) + { + // Void by default + } + } + + //-----------// + // Constants // + //-----------// + private static final class Constants + extends ConstantSet + { + //~ Instance fields ---------------------------------------------------- + + Constant.Integer halfWindowSize = new Constant.Integer( + "Pixels", + 18, + "Half size of window around a given pixel"); + + Constant.Ratio meanCoeff = new Constant.Ratio( + 0.7, + "Threshold formula coefficient for mean pixel value"); + + Constant.Ratio stdDevCoeff = new Constant.Ratio( + 0.9, + "Threshold formula coefficient for pixel standard deviation"); + + Constant.String className = new Constant.String( + "omr.run.VerticalFilter", + "omr.run.VerticalFilter or omr.run.RandomFilter"); + + } +} diff --git a/src/main/omr/run/FilterDescriptor.java b/src/main/omr/run/FilterDescriptor.java new file mode 100644 index 0000000..c4bb1fb --- /dev/null +++ b/src/main/omr/run/FilterDescriptor.java @@ -0,0 +1,204 @@ +//----------------------------------------------------------------------------// +// // +// F i l t e r D e s c r i p t o r // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.run; + +import omr.constant.ConstantSet; + +import omr.util.Param; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; + +/** + * Management data meant to describe an implementation instance of + * a PixelFilter. + * (kind of filter + related parameters) + */ +public abstract class FilterDescriptor +{ + //~ Static fields/initializers --------------------------------------------- + + /** Specific application parameters */ + private static final Constants constants = new Constants(); + + /** Usual logger utility */ + private static final Logger logger = LoggerFactory.getLogger(FilterDescriptor.class); + + /** Default param. */ + public static final Param defaultFilter = new Default(); + + //~ Methods ---------------------------------------------------------------- + // + //---------// + // getKind // + //---------// + /** + * Report the kind of filter used. + * + * @return the filter kind + */ + public abstract FilterKind getKind (); + + //----------------// + // getDefaultKind // + //----------------// + public static FilterKind getDefaultKind () + { + return constants.defaultKind.getValue(); + } + + //----------------// + // setDefaultKind // + //----------------// + public static void setDefaultKind (FilterKind kind) + { + constants.defaultKind.setValue(kind); + } + + //-----------// + // getFilter // + //-----------// + /** + * Create a filter instance compatible with the descriptor and + * the underlying pixel source. + * + * @param source the underlying pixel source + * @return the filter instance, ready to use + */ + public abstract PixelFilter getFilter (PixelSource source); + + //--------// + // equals // + //--------// + @Override + public boolean equals (Object obj) + { + return (obj instanceof FilterDescriptor); + } + + //----------// + // hashCode // + //----------// + @Override + public int hashCode () + { + int hash = 5; + return hash; + } + + //----------// + // toString // + //----------// + @Override + public String toString () + { + StringBuilder sb = new StringBuilder("{"); + sb.append(internalsString()); + sb.append('}'); + + return sb.toString(); + } + + //-----------------// + // internalsString // + //-----------------// + protected String internalsString () + { + StringBuilder sb = new StringBuilder(); + sb.append(getKind()); + + return sb.toString(); + } + + //~ Inner Classes ---------------------------------------------------------- + // + //-----------// + // Constants // + //-----------// + private static final class Constants + extends ConstantSet + { + //~ Instance fields ---------------------------------------------------- + + FilterKind.Constant defaultKind = new FilterKind.Constant( + FilterKind.GLOBAL, + "Default kind of PixelFilter"); + + } + + //---------// + // Default // + //---------// + private static class Default + extends Param + { + + @Override + public FilterDescriptor getSpecific () + { + final String method = "getDefaultDescriptor"; + + try { + FilterKind kind = getDefaultKind(); + + // Access the underlying class + Method getDesc = kind.classe.getMethod(method, (Class[]) null); + + if (Modifier.isStatic(getDesc.getModifiers())) { + return (FilterDescriptor) getDesc.invoke(null); + } else { + logger.error(method + " must be static"); + } + + } catch (NoSuchMethodException | + SecurityException | + IllegalAccessException | + IllegalArgumentException | + InvocationTargetException ex) { + logger.warn("Could not call " + method, ex); + } + + return null; + } + + @Override + public boolean setSpecific (FilterDescriptor specific) + { + if (!getSpecific().equals(specific)) { + FilterKind kind = specific.getKind(); + FilterDescriptor.setDefaultKind(kind); + + switch (kind) { + case GLOBAL: + GlobalDescriptor gDesc = (GlobalDescriptor) specific; + GlobalFilter.setDefaultThreshold(gDesc.threshold); + break; + case ADAPTIVE: + AdaptiveDescriptor aDesc = (AdaptiveDescriptor) specific; + AdaptiveFilter.setDefaultMeanCoeff(aDesc.meanCoeff); + AdaptiveFilter.setDefaultStdDevCoeff(aDesc.stdDevCoeff); + break; + } + + logger.info("Default filter is now ''{}''", specific); + + return true; + } + + return false; + } + } +} diff --git a/src/main/omr/run/FilterKind.java b/src/main/omr/run/FilterKind.java new file mode 100644 index 0000000..01b82f7 --- /dev/null +++ b/src/main/omr/run/FilterKind.java @@ -0,0 +1,104 @@ +//----------------------------------------------------------------------------// +// // +// F i l t e r K i n d // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.run; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Class {@code FilterKind} handles the various kinds of + * {@link PixelFilter} implementations. + */ +public enum FilterKind +{ + + GLOBAL("Basic filter using a global threshold", GlobalFilter.class), + ADAPTIVE( + "Adaptive filter using a local threshold", + AdaptiveFilter.getImplementationClass()); + + /** Description. */ + public final String description; + + /** Implementing class. */ + public final Class classe; + + /** Usual logger utility */ + private static final Logger logger = LoggerFactory.getLogger( + FilterKind.class); + + //------------// + // FilterKind // + //------------// + FilterKind (String description, + Class classe) + { + this.description = description; + this.classe = classe; + } + + //----------// + // Constant // + //----------// + /** + * Class {@code Constant} is a {@link omr.constant.Constant}, + * meant to store a {@link FilterKind} value. + */ + public static class Constant + extends omr.constant.Constant + { + + /** + * Specific constructor, where 'unit' and 'name' are assigned later + * + * @param defaultValue the default FilterKind value + * @param description the semantic of the constant + */ + public Constant (FilterKind defaultValue, + java.lang.String description) + { + super(null, defaultValue.toString(), description); + } + + /** + * Set a new value to the constant + * + * @param val the new FilterKind value + */ + public void setValue (FilterKind val) + { + setTuple(val.toString(), val); + } + + @Override + public void setValue (java.lang.String string) + { + setValue(decode(string)); + } + + /** + * Retrieve the current constant value + * + * @return the current FilterKind value + */ + public FilterKind getValue () + { + return (FilterKind) getCachedValue(); + } + + @Override + protected FilterKind decode (java.lang.String str) + { + return FilterKind.valueOf(str); + } + } +} diff --git a/src/main/omr/run/GlobalDescriptor.java b/src/main/omr/run/GlobalDescriptor.java new file mode 100644 index 0000000..48d4d7d --- /dev/null +++ b/src/main/omr/run/GlobalDescriptor.java @@ -0,0 +1,125 @@ +//----------------------------------------------------------------------------// +// // +// G l o b a l D e s c r i p t o r // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.run; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlAttribute; +import javax.xml.bind.annotation.XmlRootElement; + +/** + * Class {@code GlobalDescriptor} describes an {@link GlobalFilter} + * + * @author Hervé Bitteur + */ +@XmlAccessorType(XmlAccessType.NONE) +@XmlRootElement(name = "global-filter") +public class GlobalDescriptor + extends FilterDescriptor +{ + //~ Instance fields -------------------------------------------------------- + + /** The threshold value for the whole pixel source. */ + @XmlAttribute(name = "threshold") + public final int threshold; + + //~ Constructors ----------------------------------------------------------- + // + //------------------// + // GlobalDescriptor // + //------------------// + /** + * Creates a new GlobalDescriptor object. + * + * @param threshold Global threshold value + */ + public GlobalDescriptor (int threshold) + { + this.threshold = threshold; + } + + //------------------// + // GlobalDescriptor // No-arg constructor meant for JAXB + //------------------// + private GlobalDescriptor () + { + threshold = 0; + } + + //~ Methods ---------------------------------------------------------------- + //--------// + // equals // + //--------// + @Override + public boolean equals (Object obj) + { + if ((obj instanceof GlobalDescriptor) && super.equals(obj)) { + GlobalDescriptor that = (GlobalDescriptor) obj; + + return this.threshold == that.threshold; + } + + return false; + } + + //------------// + // getDefault // + //------------// + public static GlobalDescriptor getDefault () + { + return new GlobalDescriptor(GlobalFilter.getDefaultThreshold()); + } + + //-----------// + // getFilter // + //-----------// + @Override + public PixelFilter getFilter (PixelSource source) + { + return new GlobalFilter(source, threshold); + } + + // + //---------// + // getKind // + //---------// + @Override + public FilterKind getKind () + { + return FilterKind.GLOBAL; + } + + //----------// + // hashCode // + //----------// + @Override + public int hashCode () + { + int hash = 5; + hash = (53 * hash) + this.threshold; + + return hash; + } + + //-----------------// + // internalsString // + //-----------------// + @Override + protected String internalsString () + { + StringBuilder sb = new StringBuilder(super.internalsString()); + sb.append(" threshold:") + .append(threshold); + + return sb.toString(); + } +} diff --git a/src/main/omr/run/GlobalFilter.java b/src/main/omr/run/GlobalFilter.java new file mode 100644 index 0000000..f5ad106 --- /dev/null +++ b/src/main/omr/run/GlobalFilter.java @@ -0,0 +1,120 @@ +//----------------------------------------------------------------------------// +// // +// G l o b a l F i l t e r // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.run; + +import omr.constant.Constant; +import omr.constant.ConstantSet; + +import net.jcip.annotations.ThreadSafe; + +/** + * Class {@code GlobalFilter} implements Interface + * {@code PixelFilter} by using a global threshold on pixel value. + * + * @author Hervé Bitteur + */ +@ThreadSafe +public class GlobalFilter + extends SourceWrapper + implements PixelFilter +{ + //~ Static fields/initializers --------------------------------------------- + + /** Specific application parameters */ + private static final Constants constants = new Constants(); + + //~ Instance fields -------------------------------------------------------- + // + /** Global threshold. */ + private final int threshold; + + //~ Constructors ----------------------------------------------------------- + // + //--------------// + // GlobalFilter // + //--------------// + /** + * Create a binary wrapper on a raw pixel source. + * + * @param source the underlying source of raw pixels + * @param threshold maximum gray level of foreground pixel + */ + public GlobalFilter (PixelSource source, + int threshold) + { + super(source); + this.threshold = threshold; + } + + //~ Methods ---------------------------------------------------------------- + //----------------------// + // getDefaultDescriptor // + //----------------------// + public static FilterDescriptor getDefaultDescriptor () + { + return GlobalDescriptor.getDefault(); + } + + //---------------------// + // getDefaultThreshold // + //---------------------// + public static int getDefaultThreshold () + { + return constants.defaultThreshold.getValue(); + } + + //---------------------// + // setDefaultThreshold // + //---------------------// + public static void setDefaultThreshold (int threshold) + { + constants.defaultThreshold.setValue(threshold); + } + + // + //------------// + // getContext // + //------------// + @Override + public Context getContext (int x, + int y) + { + return new Context(threshold); + } + + // + // -------// + // isFore // + // -------// + @Override + public boolean isFore (int x, + int y) + { + return source.getPixel(x, y) <= threshold; + } + + //~ Inner Classes ---------------------------------------------------------- + //-----------// + // Constants // + //-----------// + private static final class Constants + extends ConstantSet + { + //~ Instance fields ---------------------------------------------------- + + Constant.Integer defaultThreshold = new Constant.Integer( + "GrayLevel", + 140, + "Default threshold value (in 0..255)"); + + } +} diff --git a/src/main/omr/run/Orientation.java b/src/main/omr/run/Orientation.java new file mode 100644 index 0000000..ca9468d --- /dev/null +++ b/src/main/omr/run/Orientation.java @@ -0,0 +1,234 @@ +//----------------------------------------------------------------------------// +// // +// O r i e n t a t i o n // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.run; + +import omr.math.BasicLine; +import omr.math.Line; + +import java.awt.Point; +import java.awt.Rectangle; +import java.awt.geom.Point2D; + +/** + * Class {@code Orientation} defines orientation as horizontal or + * vertical, and provides convenient methods to convert entities between + * absolute and oriented definitions. + * + * @author Hervé Bitteur + */ +public enum Orientation +{ + + HORIZONTAL, + VERTICAL; + + //----------// + // opposite // + //----------// + /** + * Report the orientation opposite to this one + * + * @return the opposite orientation + */ + public Orientation opposite () + { + switch (this) { + case HORIZONTAL: + return VERTICAL; + + default: + case VERTICAL: + return HORIZONTAL; + } + } + + //------------// + // isVertical // + //------------// + /** + * Return true if the entity is vertical, false if horizontal. Not a very + * object-oriented approach but who cares? + * + * @return true if vertical, false otherwise + */ + public boolean isVertical () + { + return this == VERTICAL; + } + + //----------// + // oriented // + //----------// + /** + * Given a point (x, y) in the absolute space, return the corresponding + * (coord, pos) oriented point taking the lag orientation into account. + * + * @param xy the absolute (x, y) point + * @return the corresponding oriented (coord, pos) point + */ + public Point oriented (Point xy) + { + return new Point(absolute(xy)); // Since involutive + } + + //----------// + // oriented // + //----------// + /** + * Given a point (x, y) in the absolute space, return the corresponding + * (coord, pos) oriented point taking the lag orientation into account. + * + * @param xy the absolute (x, y) point + * @return the corresponding oriented (coord, pos) point + */ + public Point2D oriented (Point2D xy) + { + return absolute(xy); // Since involutive + } + + //----------// + // absolute // + //----------// + /** + * Given a (coord, pos) oriented point, return the point (x, y) in the + * absolute space taking the lag orientation into account. + * + * @param cp the oriented (coord, pos) point + * @return the corresponding absolute (x, y) point + */ + public Point absolute (Point cp) + { + if (cp == null) { + return null; + } + + switch (this) { + case HORIZONTAL: + + // Identity: coord->x, pos->y + return new Point(cp.x, cp.y); + + default: + case VERTICAL: + + // swap: coord->y, pos->x + return new Point(cp.y, cp.x); + } + } + + //----------// + // absolute // + //----------// + /** + * Given a (coord, pos) oriented point, return the point (x, y) in the + * absolute space taking the lag orientation into account. + * + * @param cp the oriented (coord, pos) point + * @return the corresponding absolute (x, y) point + */ + public Point2D.Double absolute (Point2D cp) + { + if (cp == null) { + return null; + } + + switch (this) { + case HORIZONTAL: + + // Identity + return new Point2D.Double(cp.getX(), cp.getY()); + + default: + case VERTICAL: + + // Swap: coord->y, pos->x + return new Point2D.Double(cp.getY(), cp.getX()); + } + } + + //----------// + // absolute // + //----------// + /** + * Given a (coord, pos, length, thickness) oriented rectangle, return the + * corresponding absolute rectangle. + * + * @param cplt the oriented rectangle (coord, pos, length, thickness) + * @return the corresponding absolute rectangle (x, y, width, height). + */ + public Rectangle absolute (Rectangle cplt) + { + if (cplt == null) { + return null; + } + + switch (this) { + case HORIZONTAL: + + // coord->x, pos->y, length->width, thickness->height + return new Rectangle(cplt); + + default: + case VERTICAL: + + // coord->y, pos->x, length->height, thickness->width + return new Rectangle(cplt.y, cplt.x, cplt.height, cplt.width); + } + } + + //-----------// + // switchRef // + //-----------// + /** + * Given an oriented line, return the corresponding absolute line, or vice + * versa. + * + * @param relLine the oriented line + * @return the corresponding absolute line. + */ + public Line switchRef (Line relLine) + { + if (relLine == null) { + return null; + } + + switch (this) { + case HORIZONTAL: + + Line absLine = new BasicLine(); + absLine.includeLine(relLine); + + return absLine; + + default: + case VERTICAL: + return relLine.swappedCoordinates(); + } + } + + //----------// + // oriented // + //----------// + /** + * Given an absolute rectangle (x, y, width, height) return the + * corresponding oriented rectangle (coord, pos, length, thickness). + * + * @param xywh absolute rectangle (x, y, width, height). + * @return the corresponding oriented rectangle (coord, pos, length, + * thickness) + */ + public Rectangle oriented (Rectangle xywh) + { + // Use the fact that 'absolute' is involutive + return new Rectangle(absolute(xywh)); + } +} diff --git a/src/main/omr/run/Oriented.java b/src/main/omr/run/Oriented.java new file mode 100644 index 0000000..6739aa1 --- /dev/null +++ b/src/main/omr/run/Oriented.java @@ -0,0 +1,30 @@ +//----------------------------------------------------------------------------// +// // +// O r i e n t e d // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.run; + +/** + * Interface {@code Oriented} flags an entity as having some + * orientation. + * + * @author Hervé Bitteur + */ +public interface Oriented +{ + //~ Methods ---------------------------------------------------------------- + + /** + * Report the orientation constant + * + * @return HORIZONTAL or VERTICAL + */ + Orientation getOrientation (); +} diff --git a/src/main/omr/run/PixelFilter.java b/src/main/omr/run/PixelFilter.java new file mode 100644 index 0000000..aec63ac --- /dev/null +++ b/src/main/omr/run/PixelFilter.java @@ -0,0 +1,68 @@ +//----------------------------------------------------------------------------// +// // +// P i x e l F i l t e r // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.run; + +/** + * Interface {@code PixelFilter} reports the foreground pixels of a + * {@link PixelSource}. + * + * @author Hervé Bitteur + */ +public interface PixelFilter + extends PixelSource +{ + //~ Methods ---------------------------------------------------------------- + + /** + * Report the source context at provided location. + * This is meant for administration and display purposes, it does not need + * to be very efficient. + * + * @param x abscissa value + * @param y ordinate value + * @return the contextual data at this location + */ + Context getContext (int x, + int y); + + /** + * Report whether the pixel at location (x,y) is a foreground pixel + * or not. + * It is assumed that this feature is efficiently implemented, since it will + * be typically called several million times. + * + * @param x abscissa value + * @param y ordinate value + * @return true for a foreground pixel, false for a background pixel + */ + boolean isFore (int x, + int y); + + //~ Inner Classes ---------------------------------------------------------- + /** + * Structure used to report precise context of the source. + * It can be extended for more specialized data. + */ + class Context + { + //~ Instance fields ---------------------------------------------------- + + /** Threshold used on pixel value. */ + public final double threshold; + + //~ Constructors ------------------------------------------------------- + public Context (double threshold) + { + this.threshold = threshold; + } + } +} diff --git a/src/main/omr/run/PixelSource.java b/src/main/omr/run/PixelSource.java new file mode 100644 index 0000000..8e6ef60 --- /dev/null +++ b/src/main/omr/run/PixelSource.java @@ -0,0 +1,57 @@ +//----------------------------------------------------------------------------// +// // +// R a w P i x e l S o u r c e // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.run; + +/** + * Interface {@code PixelSource} defines the operations expected + * from a rectangular pixel source, limited by its width and height. + *

It is a raw pixel source, because just the pixel gray value + * is returned, with no interpretation as foreground or background. + * This additional interpretation is reported by a {@link PixelSource}. + * + * @author Hervé Bitteur + */ +public interface PixelSource +{ + //~ Static fields/initializers --------------------------------------------- + + /** Default value for background pixel. */ + public static final int BACKGROUND = 255; + + //~ Methods ---------------------------------------------------------------- + /** + * Report the height of the rectangular source + * + * @return the source height + */ + int getHeight (); + + /** + * Report the pixel element, as read at location (x, y) in the + * source. + * + * @param x abscissa value + * @param y ordinate value + * + * @return the pixel value using range 0..255 (0/black for foreground, + * 255/white for background) + */ + int getPixel (int x, + int y); + + /** + * Report the width of the rectangular source. + * + * @return the source width + */ + int getWidth (); +} diff --git a/src/main/omr/run/PixelsBuffer.java b/src/main/omr/run/PixelsBuffer.java new file mode 100644 index 0000000..e5ebb61 --- /dev/null +++ b/src/main/omr/run/PixelsBuffer.java @@ -0,0 +1,121 @@ +//----------------------------------------------------------------------------// +// // +// P i x e l s B u f f e r // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.run; + +import omr.run.GlobalDescriptor; + +import net.jcip.annotations.ThreadSafe; + +import java.awt.Dimension; +import java.util.Arrays; + +/** + * Class {@code PixelsBuffer} handles a plain rectangular buffer of + * chars. + * It is an efficient {@link PixelFilter} both for writing and for reading. + * + * @author Hervé Bitteur + */ +@ThreadSafe +public class PixelsBuffer + implements PixelFilter +{ + //~ Instance fields -------------------------------------------------------- + + /** Width of the table */ + private final int width; + + /** Height of the table */ + private final int height; + + /** Underlying buffer */ + private char[] buffer; + + //~ Constructors ----------------------------------------------------------- + //--------------// + // PixelsBuffer // + //--------------// + /** + * Creates a new PixelsBuffer object. + * + * @param dimension the buffer dimension + */ + public PixelsBuffer (Dimension dimension) + { + width = dimension.width; + height = dimension.height; + + buffer = new char[width * height]; + + // Initialize the whole buffer with background color value + Arrays.fill(buffer, (char) BACKGROUND); + } + + //~ Methods ---------------------------------------------------------------- + //------------// + // getContext // + //------------// + @Override + public Context getContext (int x, + int y) + { + return new Context(BACKGROUND / 2); + } + + //-----------// + // getHeight // + //-----------// + @Override + public int getHeight () + { + return height; + } + + //----------// + // getPixel // + //----------// + @Override + public int getPixel (int x, + int y) + { + return buffer[(y * width) + x]; + } + + //----------// + // getWidth // + //----------// + @Override + public int getWidth () + { + return width; + } + + //--------// + // isFore // + //--------// + @Override + public boolean isFore (int x, + int y) + { + return getPixel(x, y) != BACKGROUND; + } + + //----------// + // setPixel // + //----------// + public void setPixel (int x, + int y, + char val) + { + buffer[(y * width) + x] = val; + } +} diff --git a/src/main/omr/run/RandomFilter.java b/src/main/omr/run/RandomFilter.java new file mode 100644 index 0000000..40e031b --- /dev/null +++ b/src/main/omr/run/RandomFilter.java @@ -0,0 +1,93 @@ +//----------------------------------------------------------------------------// +// // +// R a n d o m F i l t e r // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.run; + +import net.jcip.annotations.ThreadSafe; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Class {@code RandomFilter} is a specialization of + * {@link AdaptiveFilter} which computes mean and standard + * deviation values based on pre-populated tables of integrals. + * + *

This implementation is ThreadSafe and provides fast random access to any + * location in constant time. + * The drawback is that each of the two underlying tables of integrals needs + * 8 bytes per image pixel. + * + * @author ryo/twitter @xiaot_Tag + * @author Hervé Bitteur + */ +@ThreadSafe +public class RandomFilter + extends AdaptiveFilter + implements PixelFilter +{ + //~ Static fields/initializers --------------------------------------------- + + /** Usual logger utility */ + private static final Logger logger = LoggerFactory.getLogger( + RandomFilter.class); + + //~ Constructors ----------------------------------------------------------- + // + //--------------// + // RandomFilter // + //--------------// + /** + * Create an adaptive wrapper on a raw pixel source. + * + * @param source the underlying source of raw pixels + * @param meanCoeff the coefficient for mean value + * @param stdDevCoeff the coefficient for standard deviation value + */ + public RandomFilter (PixelSource source, + double meanCoeff, + double stdDevCoeff) + { + super(source, meanCoeff, stdDevCoeff); + + // Prepare tiles + tile = new MyTile( /* squared => */ + false); + sqrTile = new MyTile( /* squared => */ + true); + } + + //~ Inner Classes ---------------------------------------------------------- + // + //--------// + // MyTile // + //--------// + /** + * This is a degenerated tile, since it is as big as the source + * image and is never shifted. + */ + private class MyTile + extends Tile + { + //~ Constructors ------------------------------------------------------- + + public MyTile (boolean squared) + { + // Allocate a tile as big as the source + super(source.getWidth(), source.getHeight(), squared); + + // Populate the whole tile at once + for (int x = 0, width = source.getWidth(); x < width; x++) { + populateColumn(x); + } + } + } +} diff --git a/src/main/omr/run/Run.java b/src/main/omr/run/Run.java new file mode 100644 index 0000000..a9f8bb4 --- /dev/null +++ b/src/main/omr/run/Run.java @@ -0,0 +1,226 @@ +//----------------------------------------------------------------------------// +// // +// R u n // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.run; + +import omr.lag.Section; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlAttribute; + +/** + * Class {@code Run} implements a contiguous run of pixels of the same + * color. Note that the direction (vertical or horizontal) is not relevant. + * + * @author Hervé Bitteur + */ +@XmlAccessorType(XmlAccessType.NONE) +public class Run +{ + //~ Instance fields -------------------------------------------------------- + + /** Number of pixels */ + @XmlAttribute + private final int length; + + /** Average pixel level along the run */ + @XmlAttribute + private final int level; + + /** Abscissa (for horizontal) / ordinate (for vertical) of first pixel */ + @XmlAttribute + private int start; + + /** Containing section, if any */ + private Section section; + + //~ Constructors ----------------------------------------------------------- + //-----// + // Run // + //-----// + /** + * Creates a new {@code Run} instance. + * + * @param start the coordinate of start for a run (y for vertical run) + * @param length the length of the run in pixels + * @param level the average level of gray in the run (0 for totally black, + * 255 for totally white) + */ + public Run (int start, + int length, + int level) + { + this.start = start; + this.length = length; + this.level = level; + } + + //-----// + // Run // + //-----// + /** Meant for XML unmarshalling only */ + private Run () + { + this(0, 0, 0); + } + + //~ Methods ---------------------------------------------------------------- + //-----------------// + // getCommonLength // + //-----------------// + /** + * Report the length of the common part with another run (assumed to be + * adjacent) + * + * @param other the other run + * @return the length of the common part + */ + public int getCommonLength (Run other) + { + int startCommon = Math.max(this.getStart(), other.getStart()); + int stopCommon = Math.min(this.getStop(), other.getStop()); + + return stopCommon - startCommon + 1; + } + + //-----------// + // getLength // + //-----------// + /** + * Report the length of the run in pixels + * + * @return this length + */ + public final int getLength () + { + return length; + } + + //----------// + // getLevel // + //----------// + /** + * Return the mean gray level of the run + * + * @return the average value of gray level along this run + */ + public final int getLevel () + { + return level; + } + + //----------// + // getStart // + //----------// + /** + * Report the starting coordinate of the run (x for horizontal, y for + * vertical) + * + * @return the start coordinate + */ + public final int getStart () + { + return start; + } + + //---------// + // getStop // + //---------// + /** + * Return the coordinate of the stop for a run. This is the bottom ordinate + * for a vertical run, or the right abscissa for a horizontal run. + * + * @return the stop coordinate + */ + public final int getStop () + { + return (start + length) - 1; + } + + //-----------// + // translate // + //-----------// + /** + * Apply a delta-coordinate translation to this run + * + * @param dc the (coordinate) translation + */ + public final void translate (int dc) + { + start += dc; + } + + //------------// + // getSection // + //------------// + /** + * Report the section that contains this run + * + * @return the containing section, or null if none + */ + public Section getSection () + { + return section; + } + + //-------------// + // isIdentical // + //-------------// + /** + * Field by field comparison + * + * @param that the other Run to compare with + * @return true if identical + */ + public boolean isIdentical (Run that) + { + return (this.start == that.start) && (this.length == that.length) + && (this.level == that.level); + } + + //------------// + // setSection // + //------------// + /** + * Records the containing section + * + * @param section the section to set + */ + public void setSection (Section section) + { + this.section = section; + } + + //----------// + // toString // + //----------// + /** + * The {@code toString} method is used to get a readable image of the + * run. + * + * @return a {@code String} value + */ + @Override + public String toString () + { + StringBuilder sb = new StringBuilder(80); + sb.append("{Run "); + sb.append(start) + .append("/") + .append(length); + sb.append("@") + .append(level); + sb.append("}"); + + return sb.toString(); + } +} diff --git a/src/main/omr/run/RunBoard.java b/src/main/omr/run/RunBoard.java new file mode 100644 index 0000000..6d2b2e8 --- /dev/null +++ b/src/main/omr/run/RunBoard.java @@ -0,0 +1,199 @@ +//----------------------------------------------------------------------------// +// // +// R u n B o a r d // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.run; + +import omr.lag.Lag; + +import omr.selection.MouseMovement; +import omr.selection.RunEvent; +import omr.selection.UserEvent; + +import omr.ui.Board; +import omr.ui.field.LIntegerField; +import omr.ui.util.Panel; + +import com.jgoodies.forms.builder.PanelBuilder; +import com.jgoodies.forms.layout.CellConstraints; +import com.jgoodies.forms.layout.FormLayout; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Class {@code RunBoard} is dedicated to display of Run information. + * + * @author Hervé Bitteur + */ +public class RunBoard + extends Board +{ + //~ Static fields/initializers --------------------------------------------- + + /** Usual logger utility */ + private static final Logger logger = LoggerFactory.getLogger( + RunBoard.class); + + /** Events this entity is interested in */ + private static final Class[] eventClasses = new Class[]{RunEvent.class}; + + //~ Instance fields -------------------------------------------------------- + /** Field for run length */ + private final LIntegerField rLength = new LIntegerField( + false, + "Length", + "Length of run in pixels"); + + /** Field for run level */ + private final LIntegerField rLevel = new LIntegerField( + false, + "Level", + "Average pixel level on this run"); + + /** Field for run start */ + private final LIntegerField rStart = new LIntegerField( + false, + "Start", + "Pixel coordinate at start of run"); + + //~ Constructors ----------------------------------------------------------- + //----------// + // RunBoard // + //----------// + /** + * Create a Run Board. + * + * @param lag the lag that encapsulates the runs table + * @param expanded true for expanded, false for collapsed + */ + public RunBoard (Lag lag, + boolean expanded) + { + this(lag.getRuns(), expanded); + } + + //----------// + // RunBoard // + //----------// + /** + * Create a Run Board. + * + * @param suffix suffix for this board + * @param lag the lag that encapsulates the runs table + * @param expanded true for expanded, false for collapsed + */ + public RunBoard (String suffix, + Lag lag, + boolean expanded) + { + this(suffix, lag.getRuns(), expanded); + } + + //----------// + // RunBoard // + //----------// + /** + * Create a Run Board. + * + * @param runsTable the table of runs + * @param expanded true for expanded, false for collapsed + */ + public RunBoard (RunsTable runsTable, + boolean expanded) + { + this("", runsTable, expanded); + } + + //----------// + // RunBoard // + //----------// + /** + * Create a Run Board. + * + * @param runsTable the table of runs + * @param expanded true for expanded, false for collapsed + */ + public RunBoard (String suffix, + RunsTable runsTable, + boolean expanded) + { + super( + Board.RUN.name + + ((runsTable.getOrientation() == Orientation.VERTICAL) ? " Vert" + : " Hori"), + Board.RUN.position + + ((runsTable.getOrientation() == Orientation.VERTICAL) ? 100 : 0), + runsTable.getRunService(), + eventClasses, + false, + expanded); + defineLayout(); + } + + //~ Methods ---------------------------------------------------------------- + //---------// + // onEvent // + //---------// + /** + * Call-back triggered when Run Selection has been modified + * + * @param event the notified event + */ + @Override + public void onEvent (UserEvent event) + { + try { + // Ignore RELEASING + if (event.movement == MouseMovement.RELEASING) { + return; + } + + logger.debug("RunBoard: {}", event); + + if (event instanceof RunEvent) { + final RunEvent runEvent = (RunEvent) event; + final Run run = runEvent.getData(); + + if (run != null) { + rStart.setValue(run.getStart()); + rLength.setValue(run.getLength()); + rLevel.setValue(run.getLevel()); + } else { + emptyFields(getBody()); + } + } + } catch (Exception ex) { + logger.warn(getClass().getName() + " onEvent error", ex); + } + } + + //--------------// + // defineLayout // + //--------------// + private void defineLayout () + { + FormLayout layout = Panel.makeFormLayout(1, 3); + PanelBuilder builder = new PanelBuilder(layout, getBody()); + builder.setDefaultDialogBorder(); + + CellConstraints cst = new CellConstraints(); + int r = 1; // -------------------------------- + + builder.add(rStart.getLabel(), cst.xy(1, r)); + builder.add(rStart.getField(), cst.xy(3, r)); + + builder.add(rLength.getLabel(), cst.xy(5, r)); + builder.add(rLength.getField(), cst.xy(7, r)); + + builder.add(rLevel.getLabel(), cst.xy(9, r)); + builder.add(rLevel.getField(), cst.xy(11, r)); + } +} diff --git a/src/main/omr/run/RunsRetriever.java b/src/main/omr/run/RunsRetriever.java new file mode 100644 index 0000000..f7f96d6 --- /dev/null +++ b/src/main/omr/run/RunsRetriever.java @@ -0,0 +1,302 @@ +//----------------------------------------------------------------------------// +// // +// R u n s R e t r i e v e r // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.run; + +import omr.step.ProcessingCancellationException; + +import omr.util.Concurrency; +import omr.util.OmrExecutors; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.awt.Rectangle; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.Callable; + +/** + * Class {@code RunsRetriever} is in charge of reading a source of + * pixels and retrieving foreground runs and background runs from it. + * What is done with the retrieved runs is essentially the purpose of the + * provided adapter. + * + * @author Hervé Bitteur + */ +public class RunsRetriever +{ + //~ Static fields/initializers --------------------------------------------- + + /** Usual logger utility */ + private static final Logger logger = LoggerFactory.getLogger(RunsRetriever.class); + + //~ Instance fields -------------------------------------------------------- + // + /** The orientation of desired runs */ + private final Orientation orientation; + + /** The adapter for pixel access and call-backs at run level */ + private final Adapter adapter; + + //~ Constructors ----------------------------------------------------------- + // + //---------------// + // RunsRetriever // + //---------------// + /** + * Creates a new RunsRetriever object. + * + * @param orientation the desired orientation + * @param adapter an adapter to provide pixel access as well as specific + * call-back actions when a run (either foreground or + * background) has just been read. + */ + public RunsRetriever (Orientation orientation, + Adapter adapter) + { + this.orientation = orientation; + this.adapter = adapter; + } + + //~ Methods ---------------------------------------------------------------- + // + //--------------// + // retrieveRuns // + //--------------// + /** + * The {@code retrieveRuns} method can be used to build the runs on + * the fly, by providing a given absolute rectangle. + * + * @param area the ABSOLUTE rectangular area to explore + */ + public void retrieveRuns (Rectangle area) + { + Rectangle rect = orientation.oriented(area); + final int cMin = rect.x; + final int cMax = (rect.x + rect.width) - 1; + final int pMin = rect.y; + final int pMax = (rect.y + rect.height) - 1; + + rowBasedRetrieval(pMin, pMax, cMin, cMax); + adapter.terminate(); + } + + //-----------------// + // processPosition // + //-----------------// + /** + * Process the pixels in position 'p' between coordinates 'cMin' + * and 'cMax' + * + * @param p the position in the pixels array (x for vertical) + * @param cMin the starting coordinate (y for vertical) + * @param cMax the ending coordinate + */ + private void processPosition (int p, + int cMin, + int cMax) + { + // Current run is FOREGROUND or BACKGROUND + boolean isFore = false; + + // Current length of the run in progress + int length = 0; + + // Current cumulated gray level for the run in progress + int cumul = 0; + + // Browse other dimension + for (int c = cMin; c <= cMax; c++) { + final int level = adapter.getLevel(c, p); + + ///logger.info("p:" + p + " c:" + c + " level:" + level); + if (adapter.isFore(c, p)) { + // We are on a foreground pixel + if (isFore) { + // Append to the foreground run in progress + length++; + cumul += level; + } else { + // End the previous background run if any + if (length > 0) { + adapter.backRun(c, p, length); + } + + // Initialize values for the starting foreground run + isFore = true; + length = 1; + cumul = level; + } + } else { + // We are on a background pixel + if (isFore) { + // End the previous foreground run + adapter.foreRun(c, p, length, cumul); + + // Initialize values for the starting background run + isFore = false; + length = 1; + } else { + // Append to the background run in progress + length++; + } + } + } + + // Process end of last run in this position + if (isFore) { + adapter.foreRun(cMax + 1, p, length, cumul); + } else { + adapter.backRun(cMax + 1, p, length); + } + } + + //-------------------// + // rowBasedRetrieval // + //-------------------// + /** + * Retrieve runs row by row. + * This method handles the pixels run either in a parallel or a serial way, + * according to the possibilities of the high OMR executor. + */ + private void rowBasedRetrieval (int pMin, + int pMax, + final int cMin, + final int cMax) + { + if (OmrExecutors.defaultParallelism.getSpecific() == false + || !adapter.isThreadSafe()) { + // Sequential + for (int p = pMin; p <= pMax; p++) { + processPosition(p, cMin, cMax); + } + } else { + // Parallel (TODO: should use Java 7 fork/join someday...) + try { + // Browse one dimension + List> tasks = new ArrayList<>( + pMax - pMin + 1); + + for (int p = pMin; p <= pMax; p++) { + final int pp = p; + tasks.add( + new Callable() + { + @Override + public Void call () + throws Exception + { + processPosition(pp, cMin, cMax); + + return null; + } + }); + } + + // Launch the tasks and wait for their completion + OmrExecutors.getHighExecutor() + .invokeAll(tasks); + } catch (InterruptedException ex) { + logger.warn("ParallelRuns got interrupted"); + throw new ProcessingCancellationException(ex); + } catch (ProcessingCancellationException pce) { + throw pce; + } catch (Throwable ex) { + logger.warn("Exception raised in ParallelRuns", ex); + throw new RuntimeException(ex); + } + } + } + + //~ Inner Interfaces ------------------------------------------------------- + // + //---------// + // Adapter // + //---------// + /** + * Interface {@code Adapter} is used to plug call-backs to a run + * retrieval process. + */ + public static interface Adapter + extends Concurrency + { + //---------// + // backRun // + //---------// + + /** + * Called at end of a background run, with the related coordinates + * + * @param coord location of the point past the end of the run + * @param pos constant position of the run + * @param length length of the run just found + */ + void backRun (int coord, + int pos, + int length); + + //---------// + // foreRun // + //---------// + /** + * Same as background, but for a foreground run. We also provide the + * measure of accumulated gray level in that case. + * + * @param coord location of the point past the end of the run + * @param pos constant position of the run + * @param length length of the run just found + * @param cumul cumulated gray levels along the run + */ + void foreRun (int coord, + int pos, + int length, + int cumul); + + //----------// + // getLevel // + //----------// + /** + * This method is used to report the gray level of the pixel + * read at location (coord, pos). + * + * @param coord x for horizontal runs, y for vertical runs + * @param pos y for horizontal runs, x for vertical runs + * + * @return the pixel gray value (from 0 for black up to 255 for white) + */ + int getLevel (int coord, + int pos); + + //--------// + // isFore // + //--------// + /** + * This method is used to check if the pixel at location + * (coord, pos) is a foreground pixel. + * + * @param coord x for horizontal runs, y for vertical runs + * @param pos y for horizontal runs, x for vertical runs + * + * @return true if pixel is foreground, false otherwise + */ + boolean isFore (int coord, + int pos); + + //-----------// + // terminate // + //-----------// + /** + * Called at the very end of run retrieval. + */ + void terminate (); + } +} diff --git a/src/main/omr/run/RunsTable.java b/src/main/omr/run/RunsTable.java new file mode 100644 index 0000000..6d8ddf2 --- /dev/null +++ b/src/main/omr/run/RunsTable.java @@ -0,0 +1,723 @@ +//----------------------------------------------------------------------------// +// // +// R u n s T a b l e // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.run; + +import omr.selection.LocationEvent; +import omr.selection.MouseMovement; +import omr.selection.RunEvent; +import omr.selection.SelectionHint; +import omr.selection.SelectionService; + +import omr.util.Predicate; + +import org.bushe.swing.event.EventSubscriber; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.awt.Dimension; +import java.awt.Point; +import java.awt.Rectangle; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +/** + * Class {@code RunsTable} handles a rectangular assembly of oriented + * runs. + * + * @author Hervé Bitteur + */ +public class RunsTable + implements Cloneable, + PixelSource, + Oriented, + EventSubscriber +{ + //~ Static fields/initializers --------------------------------------------- + + /** Usual logger utility */ + private static final Logger logger = LoggerFactory.getLogger(RunsTable.class); + + /** Events that can be published on the table run service */ + public static final Class[] eventsWritten = new Class[]{RunEvent.class}; + + /** Events observed on location service */ + public static final Class[] eventsRead = new Class[]{LocationEvent.class}; + + //~ Instance fields -------------------------------------------------------- + /** (Debugging) name of this runs table */ + private final String name; + + /** Orientation, the same for this table and all contained runs */ + private final Orientation orientation; + + /** Absolute dimension of the table */ + private final Dimension dimension; + + /** List of Runs found in each row. This is a list of lists of Runs */ + private final List> runs; + + /** Hosted event service for UI events related to this table (Runs) */ + private final SelectionService runService; + + //~ Constructors ----------------------------------------------------------- + //-----------// + // RunsTable // + //-----------// + /** + * Creates a new RunsTable object. + * + * @param name name for debugging + * @param orientation orientation of each run + * @param dimension absolute dimensions of the table (width is horizontal, + * height is vertical) + */ + public RunsTable (String name, + Orientation orientation, + Dimension dimension) + { + this.name = name; + this.orientation = orientation; + this.dimension = dimension; + + + runService = new SelectionService(name, eventsWritten); + + // Allocate the runs, according to orientation + Rectangle rect = orientation.oriented( + new Rectangle(0, 0, dimension.width, dimension.height)); + + // Prepare the collections of runs, one collection per pos value + runs = new ArrayList<>(rect.height); + + for (int i = 0; i < rect.height; i++) { + runs.add(new ArrayList()); + } + } + + //~ Methods ---------------------------------------------------------------- + //------// + // copy // + //------// + /** + * Make a copy of the table, but sharing the run instances + * + * @return another table on the same run instances + */ + public RunsTable copy () + { + return copy(name + "(copy)"); + } + + //-------// + // copy // + //-------// + /** + * Make a copy of the table, but sharing the run instances + * + * @param name a new name for the copy + * @return another table on the same run instances + */ + public RunsTable copy (String name) + { + RunsTable clone = new RunsTable(name, orientation, dimension); + + for (int i = 0; i < getSize(); i++) { + List seq = getSequence(i); + List cloneSeq = clone.getSequence(i); + + for (Run run : seq) { + cloneSeq.add(run); + } + } + + return clone; + } + + //--------// + // dumpOf // + //--------// + /** + * Report the image of the runs table. + */ + public String dumpOf () + { + StringBuilder sb = new StringBuilder(); + + sb.append(String.format("%s%n", this)); + + // Prepare output buffer + PixelsBuffer buffer = getBuffer(); + + // Print the buffer + sb.append('+'); + + for (int c = 0; c < dimension.width; c++) { + sb.append('='); + } + + sb.append(String.format("+%n")); + + for (int row = 0; row < dimension.height; row++) { + sb.append('|'); + + for (int col = 0; col < buffer.getWidth(); col++) { + sb.append((buffer.getPixel(col, row) == BACKGROUND) ? '-' : 'X'); + } + + sb.append(String.format("|%n")); + } + + sb.append('+'); + + for (int c = 0; c < dimension.width; c++) { + sb.append('='); + } + + sb.append(String.format("+%n")); + + return sb.toString(); + } + + //----------// + // getPixel // + //----------// + /** + * {@inheritDoc} + * + *
Beware, this implementation is not efficient enough + * for bulk operations. + * For such needs, a much more efficient way is to first + * retrieve a full buffer, via {@link #getBuffer()} method, then use this + * temporary buffer as the {@link PixelSource} instead of this table. + * + * @param x absolute abscissa + * @param y absolute ordinate + * @return the pixel gray level + */ + @Override + public final int getPixel (int x, + int y) + { + Run run = getRunAt(x, y); + + return (run != null) ? run.getLevel() : BACKGROUND; + } + + //----------// + // getRunAt // + //----------// + /** + * Report the run found at given coordinates, if any. + * + * @param x absolute abscissa + * @param y absolute ordinate + * @return the run found, or null otherwise + */ + public final Run getRunAt (int x, + int y) + { + Point oPt = orientation.oriented(new Point(x, y)); + + // Protection + if ((oPt.y < 0) || (oPt.y >= runs.size())) { + return null; + } + + List seq = getSequence(oPt.y); + + for (Run run : seq) { + if (run.getStart() > oPt.x) { + return null; + } + + if (run.getStop() >= oPt.x) { + return run; + } + } + + return null; + } + + //-----------// + // getBuffer // + //-----------// + /** + * Fill a rectangular buffer with the runs + * + * @return the filled buffer + */ + public PixelsBuffer getBuffer () + { + // Prepare output buffer + PixelsBuffer buffer = new PixelsBuffer(dimension); + + switch (orientation) { + case HORIZONTAL: + + for (int row = 0; row < getSize(); row++) { + List seq = getSequence(row); + + for (Run run : seq) { + for (int c = run.getStart(); c <= run.getStop(); c++) { + buffer.setPixel(c, row, (char) 0); + } + } + } + + break; + + case VERTICAL: + + for (int row = 0; row < getSize(); row++) { + List seq = getSequence(row); + + for (Run run : seq) { + for (int col = run.getStart(); col <= run.getStop(); + col++) { + buffer.setPixel(row, col, (char) 0); + } + } + } + + break; + } + + return buffer; + } + + //-------------// + // getSequence // + //-------------// + /** + * Report the sequence of runs at a given index + * + * @param index the desired index + * @return the MODIFIABLE sequence of rows + */ + public final List getSequence (int index) + { + return runs.get(index); + } + + //---------// + // getSize // + //---------// + /** + * Report the number of sequences of runs in the table + * + * @return the table size (in terms of sequences) + */ + public final int getSize () + { + return runs.size(); + } + + //--------------// + // getDimension // + //--------------// + /** + * Report the absolute dimension of the table, width along x axis + * and height along the y axis. + * + * @return the absolute dimension + */ + public Dimension getDimension () + { + return new Dimension(dimension); + } + + //-----------// + // getHeight // + //-----------// + @Override + public int getHeight () + { + return dimension.height; + } + + //---------// + // getName // + //---------// + /** + * @return the name + */ + public String getName () + { + return name; + } + + //----------------// + // getOrientation // + //----------------// + /** + * @return the orientation of the runs + */ + @Override + public Orientation getOrientation () + { + return orientation; + } + + //-------------// + // getRunCount // + //-------------// + /** + * Count and return the total number of runs in this table + * + * @return the run count + */ + public int getRunCount () + { + int runCount = 0; + + for (List seq : runs) { + for (Run run : seq) { + runCount += run.getLength(); + } + } + + return runCount; + } + + //---------------// + // getRunService // + //---------------// + /** + * Report the table run selection service + * + * @return the run selection service + */ + public SelectionService getRunService () + { + return runService; + } + + //----------// + // getWidth // + //----------// + @Override + public int getWidth () + { + return dimension.width; + } + + //---------// + // include // + //---------// + /** + * Include the content of the provided table into this one + * + * @param that the table of runs to include into this one + */ + public void include (RunsTable that) + { + if (that == null) { + throw new IllegalArgumentException( + "Cannot include a null runsTable"); + } + + if (that.orientation != orientation) { + throw new IllegalArgumentException( + "Cannot include a runsTable of different orientation"); + } + + if (!that.dimension.equals(dimension)) { + throw new IllegalArgumentException( + "Cannot include a runsTable of different dimension"); + } + + for (int row = 0; row < getSize(); row++) { + List thisSeq = this.getSequence(row); + List thatSeq = that.getSequence(row); + + for (Run thatRun : thatSeq) { + int start = thatRun.getStart(); + int iRun = 0; + + for (; iRun < thisSeq.size(); iRun++) { + Run thisRun = thisSeq.get(iRun); + + if (thisRun.getStart() > start) { + break; + } + } + + thisSeq.add(iRun, thatRun); + } + } + } + + //-------------// + // isIdentical // + //-------------// + /** + * Field by field comparison (TODO: used by unit tests only!) + * + * @param that the other RunsTable to compare with + * @return true if identical + */ + public boolean isIdentical (RunsTable that) + { + // Check null entities + if (that == null) { + return false; + } + + if ((this.orientation == that.orientation) + && this.dimension.equals(that.dimension)) { + // Check runs + for (int row = 0; row < getSize(); row++) { + List thisSeq = getSequence(row); + List thatSeq = that.getSequence(row); + + if (thisSeq.size() != thatSeq.size()) { + return false; + } + + for (int iRun = 0; iRun < thisSeq.size(); iRun++) { + Run thisRun = thisSeq.get(iRun); + Run thatRun = thatSeq.get(iRun); + + if (!thisRun.isIdentical(thatRun)) { + return false; + } + } + } + + return true; + } else { + return false; + } + } + + //-----------// + // lookupRun // + //-----------// + /** + * Given an absolute point, retrieve the containing run if any + * + * @param point coordinates of the given point + * @return the run found, or null otherwise + */ + public Run lookupRun (Point point) + { + Point oPt = orientation.oriented(point); + + if ((oPt.y < 0) || (oPt.y >= getSize())) { + return null; + } + + for (Run run : getSequence(oPt.y)) { + if (run.getStart() > oPt.x) { + return null; + } + + if (run.getStop() >= oPt.x) { + return run; + } + } + + return null; + } + + //---------// + // onEvent // + //---------// + /** + * Interest on Location => Run + * + * @param locationEvent the interesting event + */ + @Override + public void onEvent (LocationEvent locationEvent) + { + try { + // Ignore RELEASING + if (locationEvent.movement == MouseMovement.RELEASING) { + return; + } + + logger.debug("RunsTable {}: {}", name, locationEvent); + + if (locationEvent instanceof LocationEvent) { + // Location => Run + handleEvent(locationEvent); + } + } catch (Exception ex) { + logger.warn(getClass().getName() + " onEvent error", ex); + } + } + + //-------// + // purge // + //-------// + /** + * Purge a runs table of all runs that match the provided predicate + * + * @param predicate the filter to detect runs to remove + * @return this runs table, to allow easy chaining + */ + public RunsTable purge (Predicate predicate) + { + return purge(predicate, null); + } + + //-------// + // purge // + //-------// + /** + * Purge a runs table of all runs that match the provided predicate, and + * populate the provided 'removed' table with the removed runs. + * + * @param predicate the filter to detect runs to remove + * @param removed a table to be filled, if not null, with purged runs + * @return this runs table, to allow easy chaining + */ + public RunsTable purge (Predicate predicate, + RunsTable removed) + { + // Check parameters + if (removed != null) { + if (removed.orientation != orientation) { + throw new IllegalArgumentException( + "'removed' table is of different orientation"); + } + + if (!removed.dimension.equals(dimension)) { + throw new IllegalArgumentException( + "'removed' table is of different dimension"); + } + } + + for (int i = 0; i < getSize(); i++) { + List seq = getSequence(i); + + for (Iterator it = seq.iterator(); it.hasNext();) { + Run run = it.next(); + + if (predicate.check(run)) { + it.remove(); + + if (removed != null) { + removed.getSequence(i).add(run); + } + } + } + } + + return this; + } + + //-----------// + // removeRun // + //-----------// + /** + * Remove the provided run at indicated position + * + * @param pos the position where run is to be found + * @param run the run to remove + */ + public void removeRun (int pos, + Run run) + { + List seq = getSequence(pos); + + if (!seq.remove(run)) { + throw new RuntimeException( + this + " Cannot find " + run + " at pos " + pos); + } + } + + //--------------------// + // setLocationService // + //--------------------// + public void setLocationService (SelectionService locationService) + { + for (Class eventClass : eventsRead) { + locationService.subscribeStrongly(eventClass, this); + } + } + + //--------------------// + // cutLocationService // + //--------------------// + public void cutLocationService (SelectionService locationService) + { + for (Class eventClass : eventsRead) { + locationService.unsubscribe(eventClass, this); + } + } + + //----------// + // toString // + //----------// + @Override + public String toString () + { + StringBuilder sb = new StringBuilder("{"); + sb.append(getClass().getSimpleName()); + + sb.append(" ").append(name); + + sb.append(" ").append(orientation); + + sb.append(" ").append(dimension.width).append("x").append( + dimension.height); + + // Debug + if (false) { + int count = 0; + for (List seq : runs) { + count += seq.size(); + } + sb.append(" count:").append(count); + } + + sb.append("}"); + + return sb.toString(); + } + + //-------------// + // handleEvent // + //-------------// + /** + * Interest in location => Run + * + * @param location + */ + private void handleEvent (LocationEvent locationEvent) + { + Rectangle rect = locationEvent.getData(); + + if (rect == null) { + return; + } + + SelectionHint hint = locationEvent.hint; + MouseMovement movement = locationEvent.movement; + + if (!hint.isLocation() && !hint.isContext()) { + return; + } + + if ((rect.width == 0) && (rect.height == 0)) { + Point pt = rect.getLocation(); + + // Publish Run information + Run run = getRunAt(pt.x, pt.y); + runService.publish(new RunEvent(this, hint, movement, run)); + } + } +} diff --git a/src/main/omr/run/RunsTableFactory.java b/src/main/omr/run/RunsTableFactory.java new file mode 100644 index 0000000..01f5d59 --- /dev/null +++ b/src/main/omr/run/RunsTableFactory.java @@ -0,0 +1,216 @@ +//----------------------------------------------------------------------------// +// // +// R u n s T a b l e F a c t o r y // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.run; + +import net.jcip.annotations.NotThreadSafe; +import net.jcip.annotations.ThreadSafe; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.awt.Dimension; +import java.awt.Rectangle; + +/** + * Class {@code RunsTableFactory} retrieves the runs structure out of + * a given pixel source and builds the related {@link RunsTable} + * structure. + * + * @author Hervé Bitteur + */ +public class RunsTableFactory +{ + //~ Static fields/initializers --------------------------------------------- + + /** Usual logger utility */ + private static final Logger logger = LoggerFactory.getLogger( + RunsTableFactory.class); + + //~ Instance fields -------------------------------------------------------- + // + /** The source to read runs of pixels from */ + private final PixelFilter source; + + /** The desired orientation */ + private final Orientation orientation; + + /** The minimum value for a run length to be considered */ + private final int minLength; + + /** Remember if we have to swap x and y coordinates */ + private final boolean swapNeeded; + + /** The created RunsTable */ + private RunsTable table; + + //~ Constructors ----------------------------------------------------------- + // + // ------------------// + // RunsTableFactory // + // ------------------// + /** + * Create an RunsTableFactory, with its key parameters. + * + * @param orientation the desired orientation of runs + * @param source the source to read runs from. + * Orientation parameter is used to properly access the + * source pixels. + * @param minLength the minimum length for each run + */ + public RunsTableFactory (Orientation orientation, + PixelFilter source, + int minLength) + { + this.orientation = orientation; + this.source = source; + this.minLength = minLength; + + swapNeeded = orientation.isVertical(); + } + + //~ Methods ---------------------------------------------------------------- + // + // ------------// + // createTable // + // ------------// + /** + * Report the RunsTable created with the runs retrieved from the + * provided source. + * + * @param name the name to be assigned to the table + * @return a populated RunsTable + */ + public RunsTable createTable (String name) + { + table = new RunsTable( + name, + orientation, + new Dimension(source.getWidth(), source.getHeight())); + + RunsRetriever retriever = new RunsRetriever( + orientation, + new MyAdapter()); + + retriever.retrieveRuns( + new Rectangle(0, 0, source.getWidth(), source.getHeight())); + + return table; + } + + //~ Inner Classes ---------------------------------------------------------- + // + // -----------// + // MyAdapter // + // -----------// + private class MyAdapter + implements RunsRetriever.Adapter + { + //~ Methods ------------------------------------------------------------ + + // --------// + // backRun // + // --------// + @Override + public final void backRun (int coord, + int pos, + int length) + { + // No interest in background runs + } + + // --------// + // foreRun // + // --------// + @Override + public final void foreRun (int coord, + int pos, + int length, + int cumul) + { + // We consider only runs that are longer than minLength + if (length >= minLength) { + final int level = ((2 * cumul) + length) / (2 * length); + table.getSequence(pos) + .add(new Run(coord - length, length, level)); + } + } + + // ---------// + // getLevel // + // ---------// + @Override + public final int getLevel (int coord, + int pos) + { + if (swapNeeded) { + return source.getPixel(pos, coord); + } else { + return source.getPixel(coord, pos); + } + } + + // -------// + // isFore // + // -------// + @Override + public final boolean isFore (int coord, + int pos) + { + if (swapNeeded) { + return source.isFore(pos, coord); + } else { + return source.isFore(coord, pos); + } + } + + // ----------// + // terminate // + // ----------// + @Override + public final void terminate () + { + logger.debug("{} Retrieved runs: {}", table, table.getRunCount()); + } + + //--------------// + // isThreadSafe // + //--------------// + /** + * The concurrency aspects of the adapter depends on the + * underlying PixelFilter. + * + * @return true if safe, false otherwise + */ + @Override + public boolean isThreadSafe () + { + Class classe = source.getClass(); + + // Check for @ThreadSafe annotation + ThreadSafe safe = classe.getAnnotation(ThreadSafe.class); + + if (safe != null) { + return true; + } + + // Check for @NonThreadSafe annotation + NotThreadSafe notSafe = classe.getAnnotation(NotThreadSafe.class); + + if (notSafe != null) { + return false; + } + + // No annotation: it's safer to assume no thread safety + return false; + } + } +} diff --git a/src/main/omr/run/RunsTableView.java b/src/main/omr/run/RunsTableView.java new file mode 100644 index 0000000..f461b32 --- /dev/null +++ b/src/main/omr/run/RunsTableView.java @@ -0,0 +1,225 @@ +//----------------------------------------------------------------------------// +// // +// R u n s T a b l e V i e w // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.run; + +import omr.selection.LocationEvent; +import omr.selection.MouseMovement; +import omr.selection.RunEvent; +import omr.selection.SelectionHint; +import omr.selection.SelectionService; +import omr.selection.UserEvent; + +import omr.ui.view.RubberPanel; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.awt.Color; +import java.awt.Graphics2D; +import java.awt.Point; +import java.awt.Rectangle; +import java.util.List; + +/** + * Class {@code RunsTableView} displays a view on an underlying runs + * table. + * + * @author Hervé Bitteur + */ +public class RunsTableView + extends RubberPanel +{ + //~ Static fields/initializers --------------------------------------------- + + /** Usual logger utility */ + private static final Logger logger = LoggerFactory.getLogger( + RunsTableView.class); + + //~ Instance fields -------------------------------------------------------- + /** The underlying table of runs */ + private final RunsTable table; + + //~ Constructors ----------------------------------------------------------- + //---------------// + // RunsTableView // + //---------------// + /** + * Creates a new RunsTableView object. + * + * @param table the underlying table of runs + */ + public RunsTableView (RunsTable table, + SelectionService locationService) + { + this.table = table; + setName(table.getName()); + + // Location service + setLocationService(locationService); + + // Set background color + setBackground(Color.white); + } + + //~ Methods ---------------------------------------------------------------- + //---------// + // onEvent // + //---------// + /** + * Notification about selection objects. + * We catch: + * SheetLocation (-> Run) + * + * @param event the notified event + */ + @Override + @SuppressWarnings("unchecked") + public void onEvent (UserEvent event) + { + try { + // Ignore RELEASING + if (event.movement == MouseMovement.RELEASING) { + return; + } + + // Default behavior: making point visible & drawing the markers + super.onEvent(event); + + if (event instanceof LocationEvent) { // Location => Section(s) & Run + handleEvent((LocationEvent) event); + } + } catch (Exception ex) { + logger.warn(getClass().getName() + " onEvent error", ex); + } + } + + //--------// + // render // + //--------// + /** + * Render the table in the provided Graphics context, which may be + * already scaled. + * + * @param g the graphics context + */ + @Override + public void render (Graphics2D g) + { + // Render all sections, using the colors they have been assigned + renderRuns(g); + + // Paint additional items, such as recognized items, etc... + renderItems(g); + } + + //-------------// + // renderItems // + //-------------// + /** + * Room for rendering additional items, if any. + * + * @param g the graphic context + */ + protected void renderItems (Graphics2D g) + { + // Void + } + + //------------// + // renderRuns // + //------------// + protected void renderRuns (Graphics2D g) + { + Rectangle clip = g.getClipBounds(); + + switch (table.getOrientation()) { + case HORIZONTAL: { + int minRow = Math.max(clip.y, 0); + int maxRow = Math.min((clip.y + clip.height), table.getHeight()) + - 1; + + for (int row = minRow; row <= maxRow; row++) { + List seq = table.getSequence(row); + + for (Run run : seq) { + g.setColor(runColor(run)); + g.fillRect(run.getStart(), row, run.getLength(), 1); + } + } + } + + break; + + case VERTICAL: { + int minRow = Math.max(clip.x, 0); + int maxRow = Math.min((clip.x + clip.width), table.getWidth()) - 1; + + for (int row = minRow; row <= maxRow; row++) { + List seq = table.getSequence(row); + + for (Run run : seq) { + g.setColor(runColor(run)); + g.fillRect(row, run.getStart(), 1, run.getLength()); + } + } + } + + break; + } + } + + //----------// + // runColor // + //----------// + protected Color runColor (Run run) + { + // int level = run.getLevel(); + // + // return new Color(level, level, level); + return Color.BLACK; + } + + //-------------// + // handleEvent // + //-------------// + /** + * Interest in Location => Run + * + * @param locationEvent + */ + private void handleEvent (LocationEvent locationEvent) + { + logger.debug("sheetLocation: {}", locationEvent); + + // Lookup for Run pointed by this pixel location + // Search and forward run & section info + Rectangle rect = locationEvent.getData(); + + if (rect == null) { + return; + } + + SelectionHint hint = locationEvent.hint; + MouseMovement movement = locationEvent.movement; + + if (!hint.isLocation()) { + return; + } + + Point pt = rect.getLocation(); + Run run = table.lookupRun(pt); + + // Publish Run information + table.getRunService() + .publish(new RunEvent(this, hint, movement, run)); + } +} diff --git a/src/main/omr/run/SourceWrapper.java b/src/main/omr/run/SourceWrapper.java new file mode 100644 index 0000000..12dac1f --- /dev/null +++ b/src/main/omr/run/SourceWrapper.java @@ -0,0 +1,67 @@ +//----------------------------------------------------------------------------// +// // +// S o u r c e W r a p p e r // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.run; + +/** + * Class {@code SourceWrapper} wraps a PixelSource. + * + * @author Hervé Bitteur + */ +public class SourceWrapper + implements PixelSource +{ + //~ Instance fields -------------------------------------------------------- + + /** Underlying pixel source. */ + protected final PixelSource source; + + //~ Constructors ----------------------------------------------------------- + /** + * Creates a new SourceWrapper object. + * + * @param source DOCUMENT ME! + */ + public SourceWrapper (PixelSource source) + { + this.source = source; + } + + //~ Methods ---------------------------------------------------------------- + // + //-----------// + // getHeight // + //-----------// + @Override + public int getHeight () + { + return source.getHeight(); + } + + //----------// + // getPixel // + //----------// + @Override + public int getPixel (int x, + int y) + { + return source.getPixel(x, y); + } + + //----------// + // getWidth // + //----------// + @Override + public int getWidth () + { + return source.getWidth(); + } +} diff --git a/src/main/omr/run/VerticalFilter.java b/src/main/omr/run/VerticalFilter.java new file mode 100644 index 0000000..f33b8bf --- /dev/null +++ b/src/main/omr/run/VerticalFilter.java @@ -0,0 +1,142 @@ +//----------------------------------------------------------------------------// +// // +// V e r t i c a l F i l t e r // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.run; + +import net.jcip.annotations.NotThreadSafe; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Class {@code VerticalFilter} is a specialization of + * {@link AdaptiveFilter} which computes mean and standard + * deviation values based on vertical tiles of integrals. + * + *

This implementation is meant to be functionally equivalent to + * {@link RandomFilter} with similar performances but much lower + * memory requirements. + * + *

It uses a vertical window which performs the computation in constant time, + * provided that the vertical window always moves to the right. + * Instead of a whole table of integrals, this class uses a vertical tile whose + * width equals the window size, and the height equals the picture height. + *

+ *                                              +----------------+
+ *                                              |   TILE_WIDTH   |
+ * 0---------------------------------------------+---------------+
+ * |                                             |               |
+ * |                                             |               |
+ * |                                             |               |
+ * |                                            a|              b|
+ * +---------------------------------------------+---------------+
+ * |                                             |               |
+ * |                                             |    WINDOW     |
+ * |                                             |               |
+ * |                                             |               |
+ * |                                             |       +       |
+ * |                                             |               |
+ * |                                             |               |
+ * |                                             |               |
+ * |                                            c|              d|
+ * +---------------------------------------------+---------------+
+ * 
+ * Since only the (1 + WINDOW_SIZE) last columns are relevant, a tile + * uses a circular buffer to handle only those columns. + *

+ * Drawback: the implementation of the tile as a circular buffer makes + * an instance of this class usable by only one thread at a time. + * + * @author ryo/twitter @xiaot_Tag + * @author Hervé Bitteur + */ +@NotThreadSafe +public class VerticalFilter + extends AdaptiveFilter + implements PixelFilter +{ + //~ Static fields/initializers --------------------------------------------- + + /** Usual logger utility */ + private static final Logger logger = LoggerFactory.getLogger( + VerticalFilter.class); + + //~ Constructors ----------------------------------------------------------- + // + //----------------// + // VerticalFilter // + //----------------// + /** + * Create an adaptive wrapper on a raw pixel source. + * + * @param source the underlying source of raw pixels + * @param meanCoeff the coefficient for mean value + * @param stdDevCoeff the coefficient for standard deviation value + */ + public VerticalFilter (PixelSource source, + double meanCoeff, + double stdDevCoeff) + { + super(source, meanCoeff, stdDevCoeff); + + // Prepare tiles + tile = new MyTile( /* squared => */ + false); + sqrTile = new MyTile( /* squared => */ + true); + } + + //~ Methods ---------------------------------------------------------------- + // + //----------------------// + // getDefaultDescriptor // + //----------------------// + public static FilterDescriptor getDefaultDescriptor () + { + return AdaptiveDescriptor.getDefault(); + } + + //~ Inner Classes ---------------------------------------------------------- + // + //--------// + // MyTile // + //--------// + /** + * A tile as a circular buffer limited by window width. + */ + private class MyTile + extends Tile + { + //~ Constructors ------------------------------------------------------- + + public MyTile (boolean squared) + { + super(2 + (2 * HALF_WINDOW_SIZE), source.getHeight(), squared); + } + + //~ Methods ------------------------------------------------------------ + @Override + protected void shiftTile (int x2) + { + // Make sure we don't violate the tile principle + if (x2 < xRight) { + logger.error("SlidingPixelSource can only move forward"); + throw new IllegalStateException(); + } + + // Shift tile as needed to the right + while (xRight < x2) { + xRight++; + populateColumn(xRight); + } + } + } +} diff --git a/src/main/omr/run/package.html b/src/main/omr/run/package.html new file mode 100644 index 0000000..6e89de1 --- /dev/null +++ b/src/main/omr/run/package.html @@ -0,0 +1,17 @@ + + + + + + Package omr.lag + + + +

+ The run package deals with runs, which are sequences of + foreground pixels. +

+ + + diff --git a/src/main/omr/score/DurationRetriever.java b/src/main/omr/score/DurationRetriever.java new file mode 100644 index 0000000..be388f1 --- /dev/null +++ b/src/main/omr/score/DurationRetriever.java @@ -0,0 +1,194 @@ +//----------------------------------------------------------------------------// +// // +// D u r a t i o n R e t r i e v e r // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.score; + +import omr.math.Rational; + +import omr.score.entity.Chord; +import omr.score.entity.Measure; +import omr.score.entity.MeasureId.PageBased; +import omr.score.entity.Page; +import omr.score.entity.ScoreSystem; +import omr.score.entity.Slot; +import omr.score.entity.TimeSignature.InvalidTimeSignature; +import omr.score.visitor.AbstractScoreVisitor; + +import omr.util.TreeNode; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.HashMap; +import java.util.Map; + +/** + * Class {@code DurationRetriever} can visit a page hierarchy to compute + * the actual duration of every measure + * + * @author Hervé Bitteur + */ +public class DurationRetriever + extends AbstractScoreVisitor +{ + //~ Static fields/initializers --------------------------------------------- + + /** Usual logger utility */ + private static final Logger logger = LoggerFactory.getLogger( + DurationRetriever.class); + + //~ Instance fields -------------------------------------------------------- + // + /** Map of Measure id -> Measure duration, whatever the containing part */ + private final Map measureDurations = new HashMap<>(); + + /** Pass number, since we need 2 passes per system */ + private int pass = 1; + + //~ Constructors ----------------------------------------------------------- + // + //-------------------// + // DurationRetriever // + //-------------------// + /** + * Creates a new DurationRetriever object. + */ + public DurationRetriever () + { + } + + //~ Methods ---------------------------------------------------------------- + // + //---------------// + // visit Measure // + //---------------// + @Override + public boolean visit (Measure measure) + { + try { + logger.debug("Visiting Part#{} {}", + measure.getPart().getId(), measure); + + Rational measureDur = Rational.ZERO; + + // Whole/multi rests are handled outside of slots + for (Slot slot : measure.getSlots()) { + if (slot.getStartTime() != null) { + for (Chord chord : slot.getChords()) { + Rational chordEnd = slot.getStartTime().plus(chord. + getDuration()); + + if (chordEnd.compareTo(measureDur) > 0) { + measureDur = chordEnd; + } + } + } + } + + if (!measureDur.equals(Rational.ZERO)) { + // Make sure the measure duration is not bigger than limit + if (measureDur.compareTo(measure.getExpectedDuration()) <= 0) { + measure.setActualDuration(measureDur); + } else { + measure.setActualDuration(measure.getExpectedDuration()); + } + + measureDurations.put(measure.getPageId(), measureDur); + logger.debug("{}: {}", measure.getPageId(), measureDur); + } else if (!measure.getWholeChords().isEmpty()) { + if (pass > 1) { + Rational dur = measureDurations.get(measure.getPageId()); + + if (dur != null) { + measure.setActualDuration(dur); + } else { + measure.setActualDuration( + measure.getExpectedDuration()); + } + } + } + } catch (InvalidTimeSignature ex) { + } catch (Exception ex) { + logger.warn( + getClass().getSimpleName() + " Error visiting " + measure, + ex); + } + + return false; // Dead end, we don't go deeper than measure level + } + + //------------// + // visit Page // + //------------// + /** + * Page hierarchy entry point + * + * @param page the page for which measure durations are to be computed + * @return false, since no further processing is required after this node + */ + @Override + public boolean visit (Page page) + { + // Delegate to children + page.acceptChildren(this); + + return false; // No default browsing this way + } + + //-------------// + // visit Score // + //-------------// + /** + * Score hierarchy entry point, to delegate to all pages + * + * @param score the score to process + * @return false, since no further processing is required after this node + */ + @Override + public boolean visit (Score score) + { + for (TreeNode pn : score.getPages()) { + Page page = (Page) pn; + page.accept(this); + } + + return false; // No browsing + } + + //--------------// + // visit System // + //--------------// + /** + * System processing. The rest of processing is directly delegated to the + * measures + * + * @param system visit the system to export + * @return false + */ + @Override + public boolean visit (ScoreSystem system) + { + logger.debug("Visiting {}", system); + + // 2 passes are needed, to get the actual duration of whole notes + // Since the measure duration may be specified in another system part + for (pass = 1; pass <= 2; pass++) { + logger.debug("Pass #{}", pass); + + // Browse the (SystemParts and the) Measures + system.acceptChildren(this); + + logger.debug("Durations:{}", measureDurations); + } + + return false; // No default browsing this way + } +} diff --git a/src/main/omr/score/KeySignatureVerifier.java b/src/main/omr/score/KeySignatureVerifier.java new file mode 100644 index 0000000..00ec7c7 --- /dev/null +++ b/src/main/omr/score/KeySignatureVerifier.java @@ -0,0 +1,399 @@ +//----------------------------------------------------------------------------// +// // +// K e y S i g n a t u r e V e r i f i e r // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.score; + +import omr.constant.ConstantSet; + +import omr.glyph.Evaluation; +import omr.glyph.GlyphNetwork; +import omr.glyph.Glyphs; +import omr.glyph.Grades; +import omr.glyph.Shape; +import omr.glyph.facets.Glyph; + +import omr.grid.StaffInfo; + +import omr.math.GeoUtil; + +import omr.score.entity.Barline; +import omr.score.entity.KeySignature; +import omr.score.entity.Measure; +import omr.score.entity.ScoreSystem; +import omr.score.entity.Staff; +import omr.score.entity.SystemPart; + +import omr.sheet.Scale; +import omr.sheet.SystemInfo; + +import omr.util.Predicate; +import omr.util.TreeNode; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.awt.Rectangle; +import java.util.Collection; + +/** + * Class {@code KeySignatureVerifier} verifies, at system level, that all + * vertical measures exhibit the same key signature, and correct them if + * necessary. + * + * @author Herv� Bitteur + */ +public class KeySignatureVerifier +{ + //~ Static fields/initializers --------------------------------------------- + + /** Specific application parameters */ + private static final Constants constants = new Constants(); + + /** Usual logger utility */ + private static final Logger logger = LoggerFactory.getLogger( + KeySignatureVerifier.class); + + //~ Instance fields -------------------------------------------------------- + // The system concerned + private final ScoreSystem system; + + // Total number of staves in the system + private final int staffNb; + + //~ Constructors ----------------------------------------------------------- + //----------------------// + // KeySignatureVerifier // + //----------------------// + /** + * Creates a new KeySignatureVerifier object. + * + * @param system the system at hand + */ + public KeySignatureVerifier (ScoreSystem system) + { + this.system = system; + staffNb = system.getInfo() + .getStaves() + .size(); + } + + //~ Methods ---------------------------------------------------------------- + //------------// + // verifyKeys // + //------------// + /** + * Perform verifications (and corrections when possible) when all keysigs + * have been generated for the system: The key signature must be the same + * (in terms of fifths) for the same measure index in all parts and staves. + */ + public void verifyKeys () + { + logger.debug( + "\n------------------------------------------------------"); + logger.debug("verifySystemKeys for {}", system); + + // Number of measures in the system + final int measureNb = system.getFirstPart() + .getMeasures() + .size(); + + // Verify each measure index on turn + for (int im = 0; im < measureNb; im++) { + logger.debug("measure index ={}", im); + verifyVerticalMeasure(im); + } + + logger.debug( + "\n======================================================"); + } + + //-------------// + // checkKeySig // + //-------------// + private Glyph checkKeySig (Collection glyphs, + final KeySignature bestKey) + { + logger.debug( + "Merging {} for shape {}", + Glyphs.toString(glyphs), + bestKey.getShape()); + + SystemInfo systemInfo = system.getInfo(); + Glyphs.purgeManuals(glyphs); + + if (glyphs.isEmpty()) { + return null; + } + + Glyph compound = systemInfo.buildTransientCompound(glyphs); + + // Check if a proper key sig appears in the top evaluations + Evaluation vote = GlyphNetwork.getInstance() + .rawVote( + compound, + Grades.keySigMinGrade, + new Predicate() + { + @Override + public boolean check (Shape shape) + { + return shape == bestKey.getShape(); + } + }); + + if (vote != null) { + // We now have a key sig! + logger.debug( + "{} built from {}", + vote.shape, + Glyphs.toString(glyphs)); + compound = systemInfo.addGlyph(compound); + compound.setShape(vote.shape, Evaluation.ALGORITHM); + + return compound; + } else { + logger.debug( + "{}Could not find {} in {}", + systemInfo.getLogPrefix(), + bestKey.getShape(), + Glyphs.toString(glyphs)); + + return null; + } + } + + //------------------// + // getContextString // + //------------------// + private String getContextString (int measureIndex, + int systemStaffIndex) + { + return system.getContextString() + "M" + (measureIndex + 1) + "F" + + staffOf(systemStaffIndex) + .getId(); + } + + //--------------// + // getMeasureOf // + //--------------// + private Measure getMeasureOf (int staffIndex, + int measureIndex) + { + int staffOffset = 0; + + for (TreeNode node : system.getParts()) { + SystemPart part = (SystemPart) node; + staffOffset += part.getStaves() + .size(); + + if (staffIndex < staffOffset) { + return (Measure) part.getMeasures() + .get(measureIndex); + } + } + + logger.error("Illegal systemStaffIndex: {}", staffIndex); + + return null; + } + + //-------------// + // harmonizeTo // + //-------------// + private void harmonizeTo (KeySignature bestKey, + KeySignature[] keyVector, + int iMeasure) + { + Rectangle bestBox = bestKey.getBox(); + + for (int iStaff = 0; iStaff < staffNb; iStaff++) { + KeySignature ks = keyVector[iStaff]; + + // Is this staff OK? + if ((ks != null) && ks.getKey() + .equals(bestKey.getKey())) { + continue; + } + + Staff staff = staffOf(iStaff); + StaffInfo staffInfo = staff.getInfo(); + + logger.debug( + "{} Forcing key signature to {}", + getContextString(iMeasure, iStaff), + bestKey.getKey()); + + try { + // Define the box to intersect keysig glyph(s) + int xCenter = bestBox.x + (bestBox.width / 2); + Rectangle inner = new Rectangle( + xCenter, + staffInfo.getFirstLine().yAt(xCenter) + + (staffInfo.getHeight() / 2), + 0, + 0); + inner.grow((bestBox.width / 2), (staffInfo.getHeight() / 2)); + + // Draw the box, for visual debug + SystemPart part = system.getPartAt(GeoUtil.centerOf(inner)); + Barline barline = part.getStartingBarline(); + + if (barline != null) { + Glyph line = Glyphs.firstOf( + barline.getGlyphs(), + Barline.linePredicate); + + if (line != null) { + line.addAttachment("k" + staff.getId(), inner); + } + } + + // We now must find a key sig out of these glyphs + Collection glyphs = system.getInfo() + .lookupIntersectedGlyphs( + inner); + + Glyph compound = checkKeySig(glyphs, bestKey); + + if (compound != null) { + if (ks != null) { + ks.getParent() + .getChildren() + .remove(ks); + } + + Measure measure = getMeasureOf(iStaff, iMeasure); + ks = new KeySignature(measure, staff); + ks.addGlyph(compound); + } + } catch (Exception ex) { + logger.warn("Cannot copy key", ex); + ks.addError("Cannot copy key"); + } + + // TODO deassign glyphs that do not contribute to the key ? + } + } + + //---------// + // staffOf // + //---------// + private Staff staffOf (int systemStaffIndex) + { + int staffOffset = 0; + + for (TreeNode node : system.getParts()) { + SystemPart part = (SystemPart) node; + int partStaffNb = part.getStaves() + .size(); + staffOffset += partStaffNb; + + if (systemStaffIndex < staffOffset) { + return (Staff) part.getStaves() + .get( + (partStaffNb + systemStaffIndex) - staffOffset); + } + } + + logger.error("Illegal systemStaffIndex: {}", systemStaffIndex); + + return null; + } + + //-----------------------// + // verifyVerticalMeasure // + //-----------------------// + private void verifyVerticalMeasure (int im) + { + // Retrieve a key, if any, for this measure in each staff + KeySignature[] keyVector = new KeySignature[staffNb]; + int staffOffset = 0; + boolean keyFound = false; + + for (TreeNode node : system.getParts()) { + SystemPart part = (SystemPart) node; + Measure measure = (Measure) part.getMeasures() + .get(im); + + for (TreeNode ksnode : measure.getKeySignatures()) { + KeySignature ks = (KeySignature) ksnode; + keyFound = true; + + keyVector[ks.getStaff() + .getId() - 1 + staffOffset] = ks; + } + + staffOffset += part.getStaves() + .size(); + } + + // Some keys found in this vertical measure? + if (keyFound) { + logger.debug( + "{} key(s) found in M{}", + system.getContextString(), + im); + + // Browse all staves for sharp/flat compatibility + // If compatible, adjust all keysigs to the longest + boolean compatible = true; + boolean adjustment = false; + KeySignature bestKey = null; + + for (int iStaff = 0; iStaff < staffNb; iStaff++) { + KeySignature ks = keyVector[iStaff]; + + if (ks == null) { + logger.debug("Key signatures will need to be created"); + adjustment = true; + } else if (bestKey == null) { + bestKey = ks; + } else if (!bestKey.getKey() + .equals(ks.getKey())) { + logger.debug("Key signatures will need adjustment"); + adjustment = true; + + if ((ks.getKey() * bestKey.getKey()) < 0) { + logger.debug("Non compatible key signatures"); + + compatible = false; + + break; + } else if (Math.abs(bestKey.getKey()) < Math.abs( + ks.getKey())) { + // Keep longest key + bestKey = ks; + } + } + } + + // Force key signatures to this value, if compatible + if (compatible && adjustment) { + harmonizeTo(bestKey, keyVector, im); + } + } + } + + //~ Inner Classes ---------------------------------------------------------- + //-----------// + // Constants // + //-----------// + private static final class Constants + extends ConstantSet + { + //~ Instance fields ---------------------------------------------------- + + Scale.Fraction yOffset = new Scale.Fraction( + 0.5d, + "Key signature vertical offset since staff line"); + + } +} diff --git a/src/main/omr/score/MeasureBasicNumberer.java b/src/main/omr/score/MeasureBasicNumberer.java new file mode 100644 index 0000000..7ef78b6 --- /dev/null +++ b/src/main/omr/score/MeasureBasicNumberer.java @@ -0,0 +1,100 @@ +//----------------------------------------------------------------------------// +// // +// M e a s u r e B a s i c N u m b e r e r // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.score; + +import omr.score.entity.Measure; +import omr.score.entity.Page; +import omr.score.visitor.AbstractScoreVisitor; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Class {@code MeasureBasicNumberer} visits a page hierarchy to + * assign very basic measures ids. + * These Ids are very basic (and temporary), ranging from 1 for the first + * measure in the page. + * + * @author Hervé Bitteur + */ +public class MeasureBasicNumberer + extends AbstractScoreVisitor +{ + //~ Static fields/initializers --------------------------------------------- + + /** Usual logger utility */ + private static final Logger logger = LoggerFactory.getLogger( + MeasureBasicNumberer.class); + + //~ Constructors ----------------------------------------------------------- + //----------------------// + // MeasureBasicNumberer // + //----------------------// + /** + * Creates a new MeasureBasicNumberer object. + */ + public MeasureBasicNumberer () + { + } + + //~ Methods ---------------------------------------------------------------- + //---------------// + // visit Measure // + //---------------// + @Override + public boolean visit (Measure measure) + { + try { + // Set measure id, based on a preceding measure, whatever the part + Measure precedingMeasure = measure.getPrecedingInPage(); + + if (precedingMeasure != null) { + int precedingId = precedingMeasure.getIdValue(); + measure.setPageId(precedingId + 1, false); + } else { + // Very first measure (in this page) + measure.setPageId(1, false); + } + } catch (Exception ex) { + logger.warn( + getClass().getSimpleName() + " Error visiting " + measure, + ex); + } + + return true; + } + + //------------// + // visit Page // + //------------// + @Override + public boolean visit (Page page) + { + page.acceptChildren(this); + + // Temporary value + page.setDeltaMeasureId(0); + + return false; + } + + //-------------// + // visit Score // + //-------------// + @Override + public boolean visit (Score score) + { + score.acceptChildren(this); + + return false; + } +} diff --git a/src/main/omr/score/MeasureFixer.java b/src/main/omr/score/MeasureFixer.java new file mode 100644 index 0000000..9de554a --- /dev/null +++ b/src/main/omr/score/MeasureFixer.java @@ -0,0 +1,456 @@ +//----------------------------------------------------------------------------// +// // +// M e a s u r e F i x e r // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.score; + +import omr.glyph.Shape; + +import omr.math.Rational; + +import omr.score.entity.Barline; +import omr.score.entity.Measure; +import omr.score.entity.Page; +import omr.score.entity.ScoreSystem; +import omr.score.entity.SystemPart; +import omr.score.entity.TimeSignature.InvalidTimeSignature; +import omr.score.entity.Voice; +import omr.score.visitor.AbstractScoreVisitor; + +import omr.util.TreeNode; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +/** + * Class {@code MeasureFixer} visits the score hierarchy to fix measures: + *
    + *
  • Detect implicit measures (as pickup measures)
  • + *
  • Detect first half repeat measures
  • + *
  • Detect implicit measures (as second half repeats)
  • + *
  • Detect inside barlines (empty measures)
  • + *
  • Assign final page-based Measure ids
  • + *
+ * + * @author Hervé Bitteur + */ +public class MeasureFixer + extends AbstractScoreVisitor +{ + //~ Static fields/initializers --------------------------------------------- + + /** Usual logger utility */ + private static final Logger logger = LoggerFactory.getLogger(MeasureFixer.class); + + //~ Instance fields -------------------------------------------------------- + private int im; // Current measure index in system + + private List verticals = null; // Current vertical measures + + private Rational measureTermination = null; // Current termination + + private ScoreSystem system; // Current system + + // Information to remember from previous vertical measure + private List prevVerticals = null; // Previous vertical measures + + private Rational prevMeasureTermination = null; // Previous termination + + /** The latest id assigned to a measure (in the previous system) */ + private Integer prevSystemLastId = null; + + /** The latest id assigned to a measure (in the current system) */ + private Integer lastId = null; + + //~ Constructors ----------------------------------------------------------- + //--------------// + // MeasureFixer // + //--------------// + /** + * Creates a new MeasureFixer object. + */ + public MeasureFixer () + { + } + + //~ Methods ---------------------------------------------------------------- + //------------// + // visit Page // + //------------// + @Override + public boolean visit (Page page) + { + logger.debug("{} Visiting {}", getClass().getSimpleName(), page); + page.acceptChildren(this); + + // Remember the number of measures in this page + page.computeMeasureCount(); + + // Remember the delta of measure ids in this page + page.setDeltaMeasureId( + page.getLastSystem().getLastPart().getLastMeasure().getIdValue()); + + return false; + } + + //-------------// + // visit Score // + //-------------// + @Override + public boolean visit (Score score) + { + logger.debug("{} Visiting {}", getClass().getSimpleName(), score); + score.acceptChildren(this); + + return false; + } + + //--------------// + // visit System // + //--------------// + /** + * Here, we work sequentially on "vertical" measures in this system. + * Such a vertical measure is the collection of measures, across all parts + * of the system, one below the other. They share the same id and the + * same status (such as implicit). + * + * @return false + */ + @Override + public boolean visit (ScoreSystem system) + { + logger.debug("{} Visiting {}", getClass().getSimpleName(), system); + + this.system = system; + + // First, compute voices terminations + system.acceptChildren(this); + + // Measure indices to remove + List toRemove = new ArrayList<>(); + + // Use a loop on "vertical" measures, across all system parts + final int imMax = system.getFirstRealPart().getMeasures().size() - 1; + + for (im = 0; im <= imMax; im++) { + logger.debug("im:{}", im); + verticals = verticalsOf(system, im); + + // Check if all voices in all parts exhibit the same termination + measureTermination = getMeasureTermination(); + + logger.debug("measureFinal:{}{}", + measureTermination, + (measureTermination != null) + ? ("=" + measureTermination) + : ""); + + if (isEmpty()) { + logger.debug("empty"); + + // All this vertical measure is empty (no notes/rests) + // We will merge with the following measure, if any + if (im < imMax) { + setId((lastId != null) ? (lastId + 1) + : ((prevSystemLastId != null) + ? (prevSystemLastId + 1) : 1), + false); + } + } else if (isPickup()) { + logger.debug("pickup"); + setImplicit(); + setId((lastId != null) ? (-lastId) + : ((prevSystemLastId != null) + ? (-prevSystemLastId) : 0), + false); + } else if (isSecondRepeatHalf()) { + logger.debug("secondHalf"); + + // Shorten actual duration for (non-implicit) previous measure + shortenFirstHalf(); + + setImplicit(); + setId((lastId != null) ? lastId : prevSystemLastId, true); + } else if (isRealStart()) { + logger.debug("realStart"); + merge(); // Merge with previous vertical measure + toRemove.add(im); + } else { + logger.debug("normal"); + + // Normal measure + setId((lastId != null) ? (lastId + 1) + : ((prevSystemLastId != null) + ? (prevSystemLastId + 1) : 1), + false); + } + + // For next measure + prevVerticals = verticals; + prevMeasureTermination = measureTermination; + } + + removeMeasures(toRemove, system); // Remove measures if any + + // For next system + prevSystemLastId = lastId; + lastId = null; + + return false; // Dead end + } + + //---------------// + // visit Measure // + //---------------// + @Override + public boolean visit (Measure measure) + { + // Check duration sanity in this measure + // Record forward items in voices when needed + measure.checkDuration(); + + return false; + } + + //-----------------------// + // getMeasureTermination // + //-----------------------// + private Rational getMeasureTermination () + { + Rational termination = null; + + for (Measure measure : verticals) { + if (measure.isDummy()) { + continue; + } + + for (Voice voice : measure.getVoices()) { + Rational voiceTermination = voice.getTermination(); + + if (voiceTermination != null) { + if (termination == null) { + termination = voiceTermination; + } else if (!voiceTermination.equals(termination)) { + logger.debug("Non-consistent voices terminations"); + return null; + } + } + } + } + + return termination; + } + + //---------// + // isEmpty // + //---------// + /** + * Check for an empty measure: perhaps clef and key sig, but no note + * or rest + * + * @return true if so + */ + private boolean isEmpty () + { + return verticals.get(0).getActualDuration().equals(Rational.ZERO); + } + + //----------// + // isPickup // + //----------// + /** + * Check for an implicit pickup measure at the beginning of a system + * + * @return true if so + */ + private boolean isPickup () + { + return (system.getChildIndex() == 0) && (im == 0) + && (measureTermination != null) + && (measureTermination.compareTo(Rational.ZERO) < 0); + } + + //-------------// + // isRealStart // + //-------------// + /** + * Check for a measure in second position, while following an empty + * measure + * + * @return true if so + */ + private boolean isRealStart () + { + return (im == 1) + && (prevVerticals.get(0).getActualDuration().equals( + Rational.ZERO)); + ///&& (measureTermination != null); // Too strict! + } + + //--------------------// + // isSecondRepeatHalf // + //--------------------// + /** + * Check for an implicit measure as the second half of a repeat + * sequence + * + * @return true if so + */ + private boolean isSecondRepeatHalf () + { + // Check for partial first half + if ((prevMeasureTermination == null) + || (prevMeasureTermination.compareTo(Rational.ZERO) >= 0)) { + return false; + } + + // Check for partial second half + if ((measureTermination == null) + || (measureTermination.compareTo(Rational.ZERO) >= 0)) { + return false; + } + + // Check for a suitable repeat barline in between + Measure prevMeasure = prevVerticals.get(0); + Barline barline = prevMeasure.getBarline(); + + if (barline == null) { + return false; + } + + Shape shape = barline.getShape(); + + if ((shape != Shape.RIGHT_REPEAT_SIGN) + && (shape != Shape.BACK_TO_BACK_REPEAT_SIGN)) { + return false; + } + + // Check for an exact duration sum (TODO: is this too strict?) + try { + return prevMeasureTermination.plus(measureTermination).abs().equals( + prevMeasure.getExpectedDuration()); + } catch (InvalidTimeSignature its) { + return false; + } + } + + //-------------------// + // mergeWithPrevious // + //-------------------// + /** + * We have a real start, following an empty measure. + * We need to merge the vertical measures, right into left + */ + private void merge () + { + for (int iLine = 0; iLine < verticals.size(); iLine++) { + Measure left = prevVerticals.get(iLine); + Measure right = verticals.get(iLine); + left.mergeWithRight(right); + } + } + + //----------------// + // removeMeasures // + //----------------// + /** + * Remove the vertical measures that correspond to the provided + * indices + * + * @param toRemove sequence of indices to remove, perhaps empty + * @param system the containing system + */ + private void removeMeasures (List toRemove, + ScoreSystem system) + { + if (toRemove.isEmpty()) { + return; + } + + for (TreeNode pn : system.getParts()) { + SystemPart part = (SystemPart) pn; + + int index = -1; + + for (Iterator it = part.getMeasures().iterator(); it. + hasNext();) { + index++; + it.next(); + + if (toRemove.contains(index)) { + it.remove(); + } + } + } + } + + //-------// + // setId // + //-------// + private void setId (int id, + boolean isSecondHalf) + { + logger.debug("-> id={}{}", id, isSecondHalf ? " SH" : ""); + + for (Measure measure : verticals) { + measure.setPageId(id, isSecondHalf); + } + + // Side effect: remember the numeric value as last id + lastId = id; + } + + //-------------// + // setImplicit // + //-------------// + private void setImplicit () + { + for (Measure measure : verticals) { + measure.setImplicit(); + } + } + + //------------------// + // shortenFirstHalf // + //------------------// + private void shortenFirstHalf () + { + for (Measure measure : prevVerticals) { + measure.shorten(prevMeasureTermination); + measure.setFirstHalf(true); + } + } + + //-------------// + // verticalsOf // + //-------------// + /** + * Report the sequence of vertical measures for a given index + * + * @param index the index in the parent part + * @return the vertical collection of measure with the same index + */ + private List verticalsOf (ScoreSystem system, + int index) + { + List measures = new ArrayList<>(); + + for (TreeNode node : system.getParts()) { + SystemPart part = (SystemPart) node; + measures.add((Measure) part.getMeasures().get(index)); + } + + return measures; + } +} diff --git a/src/main/omr/score/MusicXML.java b/src/main/omr/score/MusicXML.java new file mode 100644 index 0000000..fa7ed34 --- /dev/null +++ b/src/main/omr/score/MusicXML.java @@ -0,0 +1,373 @@ +//----------------------------------------------------------------------------// +// // +// M u s i c X M L // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.score; + +import omr.glyph.Shape; +import static omr.glyph.Shape.*; + +import omr.score.entity.LyricsItem; +import omr.score.entity.Note; + +import com.audiveris.proxymusic.AccidentalText; +import com.audiveris.proxymusic.AccidentalValue; +import com.audiveris.proxymusic.BarStyle; +import com.audiveris.proxymusic.DegreeTypeValue; +import com.audiveris.proxymusic.Empty; +import com.audiveris.proxymusic.EmptyPlacement; +import com.audiveris.proxymusic.KindValue; +import com.audiveris.proxymusic.ObjectFactory; +import com.audiveris.proxymusic.Step; +import com.audiveris.proxymusic.StrongAccent; +import com.audiveris.proxymusic.Syllabic; +import com.audiveris.proxymusic.UpDown; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.lang.String; // Do not remove this line! + +import javax.xml.bind.JAXBElement; + +/** + * Class {@code MusicXML} gathers symbols related to the MusicXML data + * + * @author Hervé Bitteur + */ +public class MusicXML +{ + //~ Static fields/initializers --------------------------------------------- + + /** Usual logger utility */ + private static final Logger logger = LoggerFactory.getLogger( + MusicXML.class); + + /** Names of the various note types used in MusicXML */ + private static final String[] noteTypeNames = new String[]{ + "256th", "128th", "64th", + "32nd", "16th", "eighth", + "quarter", "half", "whole", + "breve", "long" + }; + + //~ Constructors ----------------------------------------------------------- + //---------------// + // ScoreExporter // + //---------------// + /** + * Not meant to be instantiated + */ + private MusicXML () + { + } + + //~ Methods ---------------------------------------------------------------- + //------------------// + // accidentalTextOf // + //------------------// + public static AccidentalText accidentalTextOf (Shape shape) + { + ObjectFactory factory = new ObjectFactory(); + AccidentalText accidentaltext = factory.createAccidentalText(); + accidentaltext.setValue(accidentalValueOf(shape)); + + return accidentaltext; + } + + //-------------------// + // accidentalValueOf // + //-------------------// + public static AccidentalValue accidentalValueOf (Shape shape) + { + ///sharp, natural, flat, double-sharp, sharp-sharp, flat-flat + // But no double-flat ??? + if (shape == Shape.DOUBLE_FLAT) { + return AccidentalValue.FLAT_FLAT; + } else { + return AccidentalValue.valueOf(shape.toString()); + } + } + + //------------// + // barStyleOf // + //------------// + /** + * Report the MusicXML bar style for a recognized Barline shape + * + * @param shape the barline shape + * @return the bar style + */ + public static BarStyle barStyleOf (Shape shape) + { + // Bar-style contains style information. Choices are + // regular, dotted, dashed, heavy, light-light, + // light-heavy, heavy-light, heavy-heavy, and none. + switch (shape) { + case THIN_BARLINE: + case PART_DEFINING_BARLINE: + return BarStyle.REGULAR; //"light" ??? + + case DOUBLE_BARLINE: + return BarStyle.LIGHT_LIGHT; + + case FINAL_BARLINE: + case RIGHT_REPEAT_SIGN: + return BarStyle.LIGHT_HEAVY; + + case REVERSE_FINAL_BARLINE: + case LEFT_REPEAT_SIGN: + return BarStyle.HEAVY_LIGHT; + + case BACK_TO_BACK_REPEAT_SIGN: + return BarStyle.HEAVY_HEAVY; //"heavy-heavy"; ??? + } + + return BarStyle.NONE; // TO BE CHECKED ??? + } + + //-----------------------// + // getArticulationObject // + //-----------------------// + public static JAXBElement getArticulationObject (Shape shape) + { + // + ObjectFactory factory = new ObjectFactory(); + EmptyPlacement ep = factory.createEmptyPlacement(); + + switch (shape) { + case DOT_set: + case STACCATO: + return factory.createArticulationsStaccato(ep); + + case ACCENT: + return factory.createArticulationsAccent(ep); + + case STRONG_ACCENT: + + // Type for strong accent: either up (^) or down (v) + // For the time being we recognize only up ones + StrongAccent strongAccent = factory.createStrongAccent(); + + if (shape == Shape.STRONG_ACCENT) { + strongAccent.setType(UpDown.UP); + } + + return factory.createArticulationsStrongAccent(strongAccent); + + case TENUTO: + return factory.createArticulationsTenuto(ep); + + case STACCATISSIMO: + return factory.createArticulationsStaccatissimo(ep); + + /** TODO: implement related shapes + * case BREATH_MARK : + * case CAESURA : + */ + } + + logger.error("Unsupported ornament shape:{}", shape); + + return null; + } + + //-------------------// + // getDynamicsObject // + //-------------------// + public static JAXBElement getDynamicsObject (Shape shape) + { + ObjectFactory factory = new ObjectFactory(); + Empty empty = factory.createEmpty(); + + switch (shape) { + case DYNAMICS_F: + return factory.createDynamicsF(empty); + + case DYNAMICS_FF: + return factory.createDynamicsFf(empty); + + case DYNAMICS_FFF: + return factory.createDynamicsFff(empty); + + // case DYNAMICS_FFFF : + // return factory.createDynamicsFfff(empty); + // + // case DYNAMICS_FFFFF : + // return factory.createDynamicsFffff(empty); + // + // case DYNAMICS_FFFFFF : + // return factory.createDynamicsFfffff(empty); + case DYNAMICS_FP: + return factory.createDynamicsFp(empty); + + case DYNAMICS_FZ: + return factory.createDynamicsFz(empty); + + case DYNAMICS_MF: + return factory.createDynamicsMf(empty); + + case DYNAMICS_MP: + return factory.createDynamicsMp(empty); + + case DYNAMICS_P: + return factory.createDynamicsP(empty); + + case DYNAMICS_PP: + return factory.createDynamicsPp(empty); + + case DYNAMICS_PPP: + return factory.createDynamicsPpp(empty); + + // case DYNAMICS_PPPP : + // return factory.createDynamicsPppp(empty); + // + // case DYNAMICS_PPPPP : + // return factory.createDynamicsPpppp(empty); + // + // case DYNAMICS_PPPPPP : + // return factory.createDynamicsPppppp(empty); + case DYNAMICS_RF: + return factory.createDynamicsRf(empty); + + case DYNAMICS_RFZ: + return factory.createDynamicsRfz(empty); + + case DYNAMICS_SF: + return factory.createDynamicsSf(empty); + + case DYNAMICS_SFFZ: + return factory.createDynamicsSffz(empty); + + case DYNAMICS_SFP: + return factory.createDynamicsSfp(empty); + + case DYNAMICS_SFPP: + return factory.createDynamicsSfpp(empty); + + case DYNAMICS_SFZ: + return factory.createDynamicsSfz(empty); + } + + logger.error("Unsupported dynamics shape:{}", shape); + + return null; + } + + //-------------// + // getTypeName // + //-------------// + /** + * Report the name for the note type + * + * @param note the note whose type name is needed + * @return proper note type name + */ + public static String getNoteTypeName (Note note) + { + // Since quarter is at index 6 in noteTypeNames, use 2**6 = 64 + ///int dur = (64 * note.getNoteDuration()) / Note.QUARTER_DURATION; + double dur = 64 * note.getNoteDuration() + .divides(Note.QUARTER_DURATION) + .toDouble(); + int index = (int) Math.rint(Math.log(dur) / Math.log(2)); + + return noteTypeNames[index]; + } + + //-------------------// + // getOrnamentObject // + //-------------------// + public static JAXBElement getOrnamentObject (Shape shape) + { + // (((trill-mark | turn | delayed-turn | shake | + // wavy-line | mordent | inverted-mordent | + // schleifer | tremolo | other-ornament), + // accidental-mark*)*)> + ObjectFactory factory = new ObjectFactory(); + + switch (shape) { + case INVERTED_MORDENT: + return factory.createOrnamentsInvertedMordent( + factory.createMordent()); + + case MORDENT: + return factory.createOrnamentsMordent(factory.createMordent()); + + case TR: + return factory.createOrnamentsTrillMark( + factory.createEmptyTrillSound()); + + case TURN: + return factory.createOrnamentsTurn(factory.createHorizontalTurn()); + } + + logger.error("Unsupported ornament shape:{}", shape); + + return null; + } + + //-------------// + // getSyllabic // + //-------------// + public static Syllabic getSyllabic (LyricsItem.SyllabicType type) + { + return Syllabic.valueOf(type.toString()); + } + + //--------// + // kindOf // + //--------// + /** + * Convert from Audiveris ChordInfo.Kind.Type type to + * Proxymusic KindValue type + * + * @param type Audiveris enum ChordSymbol.Type + * @return Proxymusic enum KindValue + */ + public static KindValue kindOf (omr.score.entity.ChordInfo.Kind.Type type) + { + return KindValue.valueOf(type.toString()); + } + + //--------// + // stepOf // + //--------// + /** + * Convert from Audiveris Step type to Proxymusic Step type + * + * @param step Audiveris enum step + * @return Proxymusic enum step + */ + public static Step stepOf (omr.score.entity.Note.Step step) + { + return Step.fromValue(step.toString()); + } + + //--------// + // typeOf // + //--------// + /** + * Convert from Audiveris ChordInfo.Degree.DegreeType to + * Proxymusic DegreeTypeValue + * + * @param type Audiveris enum DegreeType + * @return Proxymusic enum DegreeTypeValue + */ + public static DegreeTypeValue typeOf ( + omr.score.entity.ChordInfo.Degree.DegreeType type) + { + return DegreeTypeValue.valueOf(type.toString()); + } +} diff --git a/src/main/omr/score/PageReduction.java b/src/main/omr/score/PageReduction.java new file mode 100644 index 0000000..4feb7da --- /dev/null +++ b/src/main/omr/score/PageReduction.java @@ -0,0 +1,98 @@ +//----------------------------------------------------------------------------// +// // +// P a g e R e d u c t i o n // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.score; + +import omr.score.PartConnection.Candidate; +import omr.score.PartConnection.Result; +import omr.score.entity.Page; +import omr.score.entity.ScorePart; +import omr.score.entity.SystemPart; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** + * Class {@code PageReduction} reduces the parts of each system to a list of + * parts defined at page level. + * + * @author Hervé Bitteur + */ +public class PageReduction +{ + //~ Static fields/initializers --------------------------------------------- + + /** Usual logger utility */ + private static final Logger logger = LoggerFactory.getLogger(PageReduction.class); + + //~ Instance fields -------------------------------------------------------- + /** Related page */ + private final Page page; + + //~ Constructors ----------------------------------------------------------- + /** + * Creates a new PageReduction object. + * + * @param page the page to process + */ + public PageReduction (Page page) + { + this.page = page; + } + + //~ Methods ---------------------------------------------------------------- + //--------// + // reduce // + //--------// + /** + * Process a page by merging information from the page systems + */ + public void reduce () + { + if (page.getSystems().isEmpty()) { + return; + } + + /* Connect parts across the systems */ + PartConnection connection = PartConnection.connectPageSystems(page); + + // Build part list + List scoreParts = new ArrayList<>(); + + for (Result result : connection.getResultMap().keySet()) { + scoreParts.add((ScorePart) result.getUnderlyingObject()); + } + + page.setPartList(scoreParts); + + // Make the connections: (system) SystemPart -> (page) ScorePart + Map candidateMap = connection.getCandidateMap(); + logger.debug("Candidates:{}", candidateMap.size()); + + for (Map.Entry entry : candidateMap.entrySet()) { + Candidate candidate = entry.getKey(); + SystemPart systemPart = (SystemPart) candidate.getUnderlyingObject(); + + Result result = entry.getValue(); + ScorePart scorePart = (ScorePart) result.getUnderlyingObject(); + + // Connect (system) part -> (page) part + systemPart.setScorePart(scorePart); + + // Use same ID + systemPart.setId(scorePart.getId()); + } + } +} diff --git a/src/main/omr/score/PartConnection.java b/src/main/omr/score/PartConnection.java new file mode 100644 index 0000000..e2634f4 --- /dev/null +++ b/src/main/omr/score/PartConnection.java @@ -0,0 +1,973 @@ +//----------------------------------------------------------------------------// +// // +// P a r t C o n n e c t i o n // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.score; + +import omr.score.entity.Page; +import omr.score.entity.ScorePart; +import omr.score.entity.ScoreSystem; +import omr.score.entity.SystemPart; + +import omr.util.TreeNode; + +import com.audiveris.proxymusic.PartList; +import com.audiveris.proxymusic.PartName; +import com.audiveris.proxymusic.ScorePartwise; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.math.BigInteger; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.ListIterator; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import java.util.SortedMap; +import java.util.TreeMap; + +/** + * Class {@code PartConnection} is in charge of finding the connections of parts + * across systems (and pages) so that a part always represents the same + * instrument all along the score. + * + *

This work is done across: + *

    + *
  • The various systems of a page using Audiveris ScoreSystem instances.
  • + *
  • The various pages of a score using Audiveris Page instances.
  • + *
  • The various pages of a score using Proxymusic ScorePartwise + * instances.
  • + *
+ * All together, this sums up to three different cases to handle, so we have + * taken a generic approach, abstracting the different types into Candidates and + * Results.

+ * + *

The strategy used to build Results out of Candidates is based on the + * following assumptions: + *

    + *
  • For a part of a system to be connected to a part of another system, + * they must exhibit the same count of staves.
  • + *
  • Parts cannot be swapped from one system to the other. In other words, we + * cannot have say partA followed by partB in a system, and partB followed by + * partA in another system.
  • + *
  • Additional parts appear at the top of a system, rather than at the + * bottom. So we process part connections bottom up.
  • + *
  • When possible, we use the part names (or abbreviations) to help the + * connection algorithm. (not yet fully implemented). + *
+ * + * @author Hervé Bitteur + */ +public class PartConnection +{ + //~ Static fields/initializers --------------------------------------------- + + /** Usual logger utility */ + private static final Logger logger = LoggerFactory.getLogger(PartConnection.class); + + //~ Instance fields -------------------------------------------------------- + /** Input data */ + private final Set> sequences; + + /** Record the set of candidates per result */ + private final SortedMap> resultMap = new TreeMap<>(); + + /** Record which result is mapped to which candidate */ + private final Map candidateMap = new LinkedHashMap<>(); + + //~ Constructors ----------------------------------------------------------- + //----------------// + // PartConnection // + //----------------// + /** + * Creates a new PartConnection object. + * Not meant to be called directly, use proper static methods instead: + * {@link #connectPageSystems}, + * {@link #connectScorePages} or + * {@link #connectProxyPages}. + * + * @param sequences a set of sequences of parts + */ + private PartConnection (Set> sequences) + { + this.sequences = sequences; + + connect(); + } + + //~ Methods ---------------------------------------------------------------- + //--------------------// + // connectPageSystems // + //--------------------// + /** + * Convenient method to connect parts across systems of a page. + * This method is to be used when processing one page, and simply connecting + * the parts of the systems that appear on this page. Here we work with + * Audiveris ScoreSystem entities. + * + * @param page the containing page + */ + public static PartConnection connectPageSystems (Page page) + { + // Build candidates (here, a candidate is a SystemPart) + Set> sequences = new LinkedHashSet<>(); + + for (TreeNode sn : page.getSystems()) { + ScoreSystem system = (ScoreSystem) sn; + List parts = new ArrayList<>(); + + for (TreeNode pn : system.getParts()) { + SystemPart systemPart = (SystemPart) pn; + parts.add(new SystemPartCandidate(systemPart)); + } + + sequences.add(parts); + } + + return new PartConnection(sequences); + } + + //-------------------// + // connectProxyPages // + //-------------------// + /** + * Convenient method to connect parts across pages. + * This method is to be used when merging the results of several pages. + * Here we work with ProxyMusic ScorePartwise entities, since we expect each + * page result to be provided via MusicXML. + * + * @param pages the sequence of pages, as (proxymusic) ScorePartwise + * instances + */ + public static PartConnection connectProxyPages ( + SortedMap pages) + { + // Build candidates (here a candidate is a ScorePart) + Set> sequences = new LinkedHashSet<>(); + + for (Entry entry : pages.entrySet()) { + int index = entry.getKey(); + ScorePartwise page = entry.getValue(); + PartList partList = page.getPartList(); + List parts = new ArrayList<>(); + + for (Object obj : partList.getPartGroupOrScorePart()) { + // TODO: For the time being, we ignore part-group elements. + if (obj instanceof com.audiveris.proxymusic.ScorePart) { + com.audiveris.proxymusic.ScorePart scorePart = (com.audiveris.proxymusic.ScorePart) obj; + parts.add(new PMScorePartCandidate(scorePart, page, index)); + } + } + + sequences.add(parts); + } + + return new PartConnection(sequences); + } + + //-------------------// + // connectScorePages // + //-------------------// + /** + * Convenient method to connect parts across pages. + * This method is to be used when merging the results of several pages. + * Here we work directly with Audiveris Page entities + * + * @param pages the sequence of pages, as (audiveris) Page instances + */ + public static PartConnection connectScorePages ( + SortedMap pages) + { + // Build candidates (here a candidate is a ScorePart) + Set> sequences = new LinkedHashSet<>(); + + for (Entry entry : pages.entrySet()) { + Page page = entry.getValue(); + List partList = page.getPartList(); + List parts = new ArrayList<>(); + + for (ScorePart scorePart : partList) { + parts.add(new ScorePartCandidate(scorePart, page)); + } + + sequences.add(parts); + } + + return new PartConnection(sequences); + } + + //-----------------// + // getCandidateMap // + //-----------------// + /** + * Report an unmodifiable view of which resulting part has been assigned + * to any given candidate + * + * @return the candidateMap (candidate -> assigned result) + */ + public Map getCandidateMap () + { + return Collections.unmodifiableMap(candidateMap); + } + + //--------------// + // getResultMap // + //--------------// + /** + * Report which candidate parts have been mapped to any given result + * + * @return the resultMap ((sorted) result -> (unsorted) set of candidates) + */ + public SortedMap> getResultMap () + { + return resultMap; + } + + //---------// + // connect // + //---------// + /** + * The heart of the part connection algorithm, organized to work through + * interfaces in order to use the same piece of code, when we connect + * systems of one page, or when we connect parts across several pages. + */ + private void connect () + { + /** Resulting sequence of ScorePart's */ + final List results = new ArrayList<>(); + + /** Temporary map, to record the set of candidates per result */ + final Map> rawMap = new HashMap<>(); + + // Process each sequence of parts in turn + // (typically a sequence of parts is a system) + for (List sequence : sequences) { + // Current index in results sequence (built in reverse order) + int resultIndex = -1; + + if (logger.isDebugEnabled()) { + logger.debug("Processing new sequence ..."); + + for (Candidate candidate : sequence) { + logger.debug("- {}", candidate); + } + } + + // Process the sequence in reverse order (bottom up) + for (ListIterator it = sequence.listIterator( + sequence.size()); it.hasPrevious();) { + Candidate candidate = it.previous(); + logger.debug("Processing candidate {} count:{}", + candidate, candidate.getStaffCount()); + + // Check with scoreParts currently defined + resultIndex++; + logger.debug("scorePartIndex:{}", resultIndex); + + if (resultIndex >= results.size()) { + logger.debug("No more scoreParts available"); + + // Create a brand new score part for this candidate + createResult(resultIndex, candidate, results, rawMap); + } else { + Result result = results.get(resultIndex); + logger.debug("Part:{}", result); + + // Check we are connectable in terms of staves + if (result.getStaffCount() != candidate.getStaffCount()) { + logger.debug("Count incompatibility"); + + // Create a brand new score part for this candidate + createResult(resultIndex, candidate, results, rawMap); + } else { + // Can we use names? Just for fun for the time being + if ((candidate.getName() != null) + && (result.getName() != null)) { + boolean namesOk = candidate.getName(). + equalsIgnoreCase( + result.getName()); + + logger.debug("Names OK: {}", namesOk); + + if (!namesOk) { + logger.debug("\"{}\" vs \"{}\"", + candidate.getName(), result.getName()); + } + } + + // We are compatible + candidateMap.put(candidate, result); + logger.debug("Compatible." + + " Mapped candidate {} to result {}", + candidate, result); + + rawMap.get(result).add(candidate); + } + } + } + } + + // Reverse and number ScorePart instances + Collections.reverse(results); + + for (int i = 0; i < results.size(); i++) { + Result result = results.get(i); + int id = i + 1; + result.setId(id); + + // Forge a ScorePart name if none has been assigned + if (result.getName() == null) { + result.setName("Part_" + id); + } + logger.debug("Final {}", result); + } + + // Now that results are ordered, we can deliver the sorted map + resultMap.putAll(rawMap); + } + + //--------------// + // createResult // + //--------------// + private Result createResult (int resultIndex, + Candidate candidate, + List results, + Map> rawMap) + { + Set candidates = new LinkedHashSet<>(); + Result result = candidate.createResult(); + candidateMap.put(candidate, result); + logger.debug("Creation. Mapped candidate {} to result {}", + candidate, result); + + candidates.add(candidate); + rawMap.put(result, candidates); + results.add(resultIndex, result); + + return result; + } + + //~ Inner Interfaces ------------------------------------------------------- + //-----------// + // Candidate // + //-----------// + /** + * Interface {@code Candidate} is used to process part candidates, + * regardless whether they are provided: + *
    + *
  • as Audiveris {@link omr.score.entity.SystemPart} instances + * (produced by the scanning of just one page)
  • + *
  • as Audiveris {@link omr.score.entity.ScorePart} + * (when merging Audiveris pages)
  • + *
  • as ProxyMusic {@link com.audiveris.proxymusic.ScorePart} instances + * (used when merging MusicXML files).
  • + *
+ */ + public static interface Candidate + { + //~ Methods ------------------------------------------------------------ + + /** Create a related result instance consistent with this type */ + public Result createResult (); + + /** Report the abbreviation, if any, that relates to this part */ + public String getAbbreviation (); + + /** Index of the input: System # for SystemPart, Page # for ScorePart */ + public int getInputIndex (); + + /** Report the name of the part, if any */ + public String getName (); + + /** Report the number of staves in the part */ + public int getStaffCount (); + + /** Report the underlying object */ + public Object getUnderlyingObject (); + } + + //--------// + // Result // + //--------// + /** + * Interface {@code Result} is used to process resulting ScorePart + * instances, + * regardless whether they are instances of standard Audiveris {@link + * ScorePart} or instances of ProxyMusic + * {@link com.audiveris.proxymusic.ScorePart}. + */ + public static interface Result + extends Comparable + { + //~ Methods ------------------------------------------------------------ + + /** Report the part abbreviation, if any */ + public String getAbbreviation (); + + /** Report the candidate object used to build this result */ + public Candidate getCandidate (); + + /** Report the part id */ + public int getId (); + + /** Report the part name */ + public String getName (); + + /** Report the number of staves in that part */ + public int getStaffCount (); + + /** Report the actual underlying instance */ + public Object getUnderlyingObject (); + + /** Assign an abbreviation to the part */ + public void setAbbreviation (String abbreviation); + + /** Assign an unique id to the part */ + public void setId (int id); + + /** Assign a name to the part */ + public void setName (String name); + } + + //~ Inner Classes ---------------------------------------------------------- + //----------------// + // AbstractResult // + //----------------// + private abstract static class AbstractResult + implements Result + { + //~ Instance fields ---------------------------------------------------- + + protected final Candidate candidate; + + //~ Constructors ------------------------------------------------------- + public AbstractResult (Candidate candidate) + { + this.candidate = candidate; + } + + //~ Methods ------------------------------------------------------------ + @Override + public int compareTo (Result other) + { + return Integer.signum(getId() - other.getId()); + } + + @Override + public Candidate getCandidate () + { + return candidate; + } + + @Override + public String toString () + { + StringBuilder sb = new StringBuilder("{"); + sb.append(getClass().getSimpleName()); + + sb.append(" id:").append(getId()); + + sb.append(" name:\"").append(getName()).append("\""); + + if (getAbbreviation() != null) { + sb.append(" abbr:\"").append(getAbbreviation()).append("\""); + } + + sb.append(" staffCount:").append(getStaffCount()); + + sb.append("}"); + + return sb.toString(); + } + } + + //----------------------// + // PMScorePartCandidate // + //----------------------// + /** + * Wrapping class meant for a proxymusic ScorePart instance candidate + */ + private static class PMScorePartCandidate + implements Candidate + { + //~ Instance fields ---------------------------------------------------- + + private final com.audiveris.proxymusic.ScorePart scorePart; + + private final ScorePartwise scorePartwise; + + private final int inputIndex; + + private Integer staffCount; + + //~ Constructors ------------------------------------------------------- + public PMScorePartCandidate ( + com.audiveris.proxymusic.ScorePart scorePart, + ScorePartwise scorePartwise, + int inputIndex) + { + this.scorePart = scorePart; + this.scorePartwise = scorePartwise; + this.inputIndex = inputIndex; + } + + //~ Methods ------------------------------------------------------------ + @Override + public PMScorePartResult createResult () + { + // Create a brand new score part for this candidate + // Id is irrelevant for the time being + + /** Factory for proxymusic entities */ + com.audiveris.proxymusic.ObjectFactory factory = new com.audiveris.proxymusic.ObjectFactory(); + + PMScorePartResult result = new PMScorePartResult( + this, + getStaffCount(), + factory.createScorePart()); + + result.setName(getName()); + result.setAbbreviation(getAbbreviation()); + logger.debug("Created {} from {}", result, this); + + return result; + } + + @Override + public String getAbbreviation () + { + PartName partName = scorePart.getPartAbbreviation(); + + if (partName != null) { + return partName.getValue(); + } else { + return null; + } + } + + @Override + public int getInputIndex () + { + return inputIndex; + } + + @Override + public String getName () + { + PartName partName = scorePart.getPartName(); + + if (partName != null) { + return partName.getValue(); + } else { + return null; + } + } + + @Override + public int getStaffCount () + { + if (staffCount == null) { + // Determine the corresponding staff count + // We have to dig into the first measure of the part itself + staffCount = 1; // Default value + + String id = scorePart.getId(); + + ///logger.debug("scorePart id:" + id); + for (ScorePartwise.Part part : scorePartwise.getPart()) { + if (part.getId() != scorePart) { + continue; + } + + // Get first measure of this part + ScorePartwise.Part.Measure firstMeasure = part.getMeasure(). + get(0); + + // Look for Attributes element + for (Object obj : firstMeasure.getNoteOrBackupOrForward()) { + if (!(obj instanceof com.audiveris.proxymusic.Attributes)) { + continue; + } + + com.audiveris.proxymusic.Attributes attributes = (com.audiveris.proxymusic.Attributes) obj; + BigInteger staves = attributes.getStaves(); + + if (staves != null) { + staffCount = staves.intValue(); + + break; + } + } + + break; + } + } + + return staffCount; + } + + @Override + public Object getUnderlyingObject () + { + return scorePart; + } + + @Override + public String toString () + { + StringBuilder sb = new StringBuilder("{"); + + sb.append(getClass().getSimpleName()); + + sb.append(" page#").append(inputIndex); + + sb.append(" \"").append(getName()).append("\""); + + sb.append(" staffCount:").append(getStaffCount()); + + sb.append("}"); + + return sb.toString(); + } + } + + //-------------------// + // PMScorePartResult // + //-------------------// + /** + * Wrapping class meant for a proxymusic ScorePart instance result + */ + private static class PMScorePartResult + extends AbstractResult + { + //~ Instance fields ---------------------------------------------------- + + private final int staffCount; + + private final com.audiveris.proxymusic.ScorePart scorePart; + + private int id; + + //~ Constructors ------------------------------------------------------- + public PMScorePartResult (Candidate candidate, + int staffCount, + com.audiveris.proxymusic.ScorePart scorePart) + { + super(candidate); + this.staffCount = staffCount; + this.scorePart = scorePart; + } + + //~ Methods ------------------------------------------------------------ + @Override + public String getAbbreviation () + { + PartName partName = scorePart.getPartAbbreviation(); + + if (partName != null) { + return partName.getValue(); + } else { + return null; + } + } + + @Override + public int getId () + { + return id; + } + + @Override + public String getName () + { + PartName partName = scorePart.getPartName(); + + if (partName != null) { + return partName.getValue(); + } else { + return null; + } + } + + @Override + public int getStaffCount () + { + return staffCount; + } + + @Override + public com.audiveris.proxymusic.ScorePart getUnderlyingObject () + { + return scorePart; + } + + @Override + public void setAbbreviation (String abbreviation) + { + com.audiveris.proxymusic.ObjectFactory factory = new com.audiveris.proxymusic.ObjectFactory(); + PartName partName = factory.createPartName(); + scorePart.setPartAbbreviation(partName); + partName.setValue(abbreviation); + } + + @Override + public void setId (int id) + { + this.id = id; + scorePart.setId("P" + id); + } + + @Override + public void setName (String name) + { + com.audiveris.proxymusic.ObjectFactory factory = new com.audiveris.proxymusic.ObjectFactory(); + com.audiveris.proxymusic.PartName partName = factory.createPartName(); + scorePart.setPartName(partName); + partName.setValue(name); + } + } + + //--------------------// + // ScorePartCandidate // + //--------------------// + /** + * Wrapping class meant for a ScorePart instance candidate + */ + private static class ScorePartCandidate + implements Candidate + { + //~ Instance fields ---------------------------------------------------- + + private final ScorePart scorePart; + + private final Page page; + + //~ Constructors ------------------------------------------------------- + public ScorePartCandidate (ScorePart scorePart, + Page page) + { + this.scorePart = scorePart; + this.page = page; + } + + //~ Methods ------------------------------------------------------------ + @Override + public ScorePartResult createResult () + { + // Create a brand new score part for this candidate + // Id is irrelevant for the time being + ScorePartResult result = new ScorePartResult( + this, + new ScorePart(0, getStaffCount())); + result.setName(getName()); + result.setAbbreviation(getAbbreviation()); + logger.debug("Created {} from {}", result, this); + + return result; + } + + @Override + public String getAbbreviation () + { + return scorePart.getAbbreviation(); + } + + @Override + public int getInputIndex () + { + return page.getIndex(); + } + + @Override + public String getName () + { + return scorePart.getName(); + } + + @Override + public int getStaffCount () + { + return scorePart.getStaffCount(); + } + + @Override + public Object getUnderlyingObject () + { + return scorePart; + } + + @Override + public String toString () + { + StringBuilder sb = new StringBuilder("{"); + + sb.append(getClass().getSimpleName()); + + sb.append(" page#").append(getInputIndex()); + + sb.append(" \"").append(getName()).append("\""); + + sb.append(" staffCount:").append(getStaffCount()); + + sb.append("}"); + + return sb.toString(); + } + } + + //-----------------// + // ScorePartResult // + //-----------------// + /** + * Wrapping class meant for a ScorePart instance result + */ + private static class ScorePartResult + extends AbstractResult + { + //~ Instance fields ---------------------------------------------------- + + private final ScorePart scorePart; + + //~ Constructors ------------------------------------------------------- + public ScorePartResult (Candidate candidate, + ScorePart scorePart) + { + super(candidate); + this.scorePart = scorePart; + } + + //~ Methods ------------------------------------------------------------ + @Override + public String getAbbreviation () + { + return scorePart.getAbbreviation(); + } + + @Override + public int getId () + { + return scorePart.getId(); + } + + @Override + public String getName () + { + return scorePart.getName(); + } + + @Override + public int getStaffCount () + { + return scorePart.getStaffCount(); + } + + @Override + public ScorePart getUnderlyingObject () + { + return scorePart; + } + + @Override + public void setAbbreviation (String abbreviation) + { + scorePart.setAbbreviation(abbreviation); + } + + @Override + public void setId (int id) + { + scorePart.setId(id); + } + + @Override + public void setName (String name) + { + scorePart.setName(name); + } + } + + //---------------------// + // SystemPartCandidate // + //---------------------// + /** + * Wrapping class meant for a SystemPart instance + */ + private static class SystemPartCandidate + implements Candidate + { + //~ Instance fields ---------------------------------------------------- + + private final SystemPart systemPart; + + //~ Constructors ------------------------------------------------------- + public SystemPartCandidate (SystemPart part) + { + this.systemPart = part; + } + + //~ Methods ------------------------------------------------------------ + @Override + public ScorePartResult createResult () + { + // Create a brand new score part for this candidate + // Id is irrelevant for the time being + ScorePartResult result = new ScorePartResult( + this, + new ScorePart(0, getStaffCount())); + result.setName(getName()); + result.setAbbreviation(getAbbreviation()); + logger.debug("Created {} from {}", result, this); + + return result; + } + + @Override + public String getAbbreviation () + { + return null; + } + + @Override + public int getInputIndex () + { + return systemPart.getSystem().getId(); + } + + @Override + public String getName () + { + return systemPart.getName(); + } + + @Override + public int getStaffCount () + { + return systemPart.getStaves().size(); + } + + @Override + public Object getUnderlyingObject () + { + return systemPart; + } + + @Override + public String toString () + { + return "S" + getInputIndex() + "-" + systemPart.toString(); + } + } +} diff --git a/src/main/omr/score/Score.java b/src/main/omr/score/Score.java new file mode 100644 index 0000000..d0dc421 --- /dev/null +++ b/src/main/omr/score/Score.java @@ -0,0 +1,998 @@ +//----------------------------------------------------------------------------// +// // +// S c o r e // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.score; + +import omr.Main; + +import omr.constant.Constant; +import omr.constant.ConstantSet; + +import omr.text.Language; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import omr.math.Rational; + +import omr.run.FilterDescriptor; + +import omr.score.entity.MeasureId.MeasureRange; +import omr.score.entity.Page; +import omr.score.entity.ScoreNode; +import omr.score.entity.ScorePart; +import omr.score.entity.Tempo; +import omr.score.ui.ScoreTree; +import omr.score.visitor.ScoreVisitor; + +import omr.script.ParametersTask.PartData; +import omr.script.Script; +import omr.script.ScriptActions; + +import omr.sheet.Sheet; +import omr.sheet.picture.PictureLoader; +import omr.sheet.ui.SheetActions; +import omr.sheet.ui.SheetsController; + +import omr.step.StepException; + +import omr.util.Param; +import omr.util.FileUtil; +import omr.util.TreeNode; + +import java.awt.image.RenderedImage; +import java.io.File; +import java.util.ArrayList; +import java.util.List; +import java.util.Map.Entry; +import java.util.SortedMap; +import java.util.SortedSet; + +import javax.swing.JFrame; + +/** + * Class {@code Score} handles a score hierarchy, composed of one or + * several pages. + * + * @author Hervé Bitteur + */ +public class Score + extends ScoreNode +{ + //~ Static fields/initializers --------------------------------------------- + + /** Specific application parameters */ + private static final Constants constants = new Constants(); + + /** Usual logger utility */ + private static final Logger logger = LoggerFactory.getLogger(Score.class); + + /** Number of lines in a staff */ + public static final int LINE_NB = 5; + + //~ Instance fields -------------------------------------------------------- + /** Input file of the related image(s) */ + private final File imageFile; + + /** The related file radix (name w/o extension) */ + private final String radix; + + /** True if the score contains several pages */ + private boolean multiPage; + + /** The recording of key processing data */ + private ScoreBench bench; + + /** Dominant text language in the score */ + private String language; + + /** Greatest duration divisor */ + private Integer durationDivisor; + + /** ScorePart list for the whole score */ + private List partList; + + /** The specified volume, if any */ + private Integer volume; + + /** Potential measure range, if not all score is to be played */ + private MeasureRange measureRange; + + /** Browser tree on this score */ + private ScoreTree scoreTree; + + /** Where the MusicXML output is to be stored */ + private File exportFile; + + /** Where the script is to be stored */ + private File scriptFile; + + /** Where the MIDI data is to be stored */ + private File midiFile; + + /** Where the sheet PDF data is to be stored */ + private File printFile; + + /** The script of user actions on this score */ + private Script script; + + /** Handling of binarization filter parameter. */ + private final Param filterParam = + new Param<>(FilterDescriptor.defaultFilter); + + /** Handling of tempo parameter. */ + private final Param tempoParam = + new Param<>(Tempo.defaultTempo); + + /** Handling of language parameter. */ + private final Param textParam = + new Param<>(Language.defaultSpecification); + + /** Handling of parts name and program. */ + private final Param> partsParam = new PartsParam(); + + //~ Constructors ----------------------------------------------------------- + //-------// + // Score // + //-------// + /** + * Create a Score with a path to an input image file. + * + * @param imageFile the input image file (which may contain several images) + */ + public Score (File imageFile) + { + super(null); // No container + + this.imageFile = imageFile; + radix = FileUtil.getNameSansExtension(imageFile); + + // Related bench + bench = new ScoreBench(this); + + // Register this score instance + ScoresManager.getInstance().addInstance(this); + } + + //~ Methods ---------------------------------------------------------------- + //--------// + // accept // + //--------// + @Override + public boolean accept (ScoreVisitor visitor) + { + return visitor.visit(this); + } + + //-------// + // close // + //-------// + /** + * Close this score instance, as well as its view if any. + */ + public void close () + { + logger.info("Closing {}", this); + + // Check whether the score script has been saved (or user has declined) + if ((Main.getGui() != null) && !ScriptActions.checkStored(getScript())) { + return; + } + + // Close contained sheets (and pages) + for (TreeNode pn : new ArrayList<>(getPages())) { + Page page = (Page) pn; + Sheet sheet = page.getSheet(); + sheet.remove(true); + } + + // Close tree if any + if (scoreTree != null) { + scoreTree.close(); + } + + // Complete and store all bench data + ScoresManager.getInstance().storeBench(bench, null, true); + + // Remove from score instances + ScoresManager.getInstance().removeInstance(this); + } + + //-------------// + // createPages // + //-------------// + /** + * Create as many pages (and related sheets) as there are images + * in the input image file. + * + * @param pages set of page ids (1-based) explicitly included. + * if set is empty or null all pages are loaded + */ + public void createPages (SortedSet pages) + { + SortedMap images = PictureLoader.loadImages( + imageFile, + pages); + + if (images != null) { + Page firstPage = null; + setMultiPage(images.size() > 1); // Several images in the file + + for (Entry entry : images.entrySet()) { + int index = entry.getKey(); + RenderedImage image = entry.getValue(); + Page page = null; + + try { + page = new Page(this, index, image); + + if (firstPage == null) { + firstPage = page; + + // Let the UI focus on first page + if (Main.getGui() != null) { + SheetsController.getInstance().showAssembly(firstPage. + getSheet()); + } + } + } catch (StepException ex) { + // Remove page from score, if already included + if ((page != null) && getPages().remove(page)) { + logger.info("Page #{} removed", index); + } + } + } + + // Remember (even across runs) the parent directory + ScoresManager.getInstance().setDefaultInputDirectory(getImageFile(). + getParent()); + + // Insert in sheet history + ScoresManager.getInstance().getHistory().add(getImagePath()); + if (Main.getGui() != null) { + SheetActions.HistoryMenu.getInstance().setEnabled(true); + } + } + } + + //------// + // dump // + //------// + /** + * Dump a whole score hierarchy. + */ + public void dump () + { + System.out.println( + "----------------------------------------------------------------"); + + if (dumpNode()) { + dumpChildren(1); + } + + System.out.println( + "----------------------------------------------------------------"); + } + + //----------------// + // getFilterParam // + //----------------// + public Param getFilterParam () + { + return filterParam; + } + + //----------------// + // getTempoParam // + //----------------// + public Param getTempoParam () + { + return tempoParam; + } + + //--------------// + // getTextParam // + //--------------// + public Param getTextParam () + { + return textParam; + } + + //---------------// + // getPartsParam // + //---------------// + public Param> getPartsParam () + { + return partsParam; + } + + //------------------// + // getDefaultVolume // + //------------------// + /** + * Report default value for Midi volume. + * + * @return the default volume value + */ + public static int getDefaultVolume () + { + return constants.defaultVolume.getValue(); + } + + //--------------------// + // getDurationDivisor // + //--------------------// + /** + * Report the common divisor used for this score when + * simplifying the durations. + * + * @return the computed divisor (GCD), or null if not computable + */ + public Integer getDurationDivisor () + { + if (durationDivisor == null) { + accept(new ScoreReductor()); + } + + return durationDivisor; + } + + //---------------// + // getExportFile // + //---------------// + /** + * Report to which file, if any, the score is to be exported. + * + * @return the exported xml file, or null + */ + public File getExportFile () + { + return exportFile; + } + + //--------------// + // getFirstPage // + //--------------// + public Page getFirstPage () + { + if (children.isEmpty()) { + return null; + } else { + return (Page) children.get(0); + } + } + + //--------------// + // getImageFile // + //--------------// + /** + * @return the imageFile + */ + public File getImageFile () + { + return imageFile; + } + + //--------------// + // getImagePath // + //--------------// + /** + * Report the (canonical) file name of the score image(s). + * + * @return the file name + */ + public String getImagePath () + { + return imageFile.getPath(); + } + + //--------------------// + // getMeasureIdOffset // + //--------------------// + /** + * Report the offset to add to page-based measure ids of the + * provided page to get absolute (score-based) ids. + * + * @param page the provided page + * @return the measure id offset for the page + */ + public Integer getMeasureIdOffset (Page page) + { + int offset = 0; + + for (TreeNode pn : getPages()) { + Page p = (Page) pn; + + if (p == page) { + return offset; + } else { + Integer delta = p.getDeltaMeasureId(); + + if (delta != null) { + offset += delta; + } else { + // This page has no measures yet, so ... + return null; + } + } + } + + throw new IllegalArgumentException(page + " not found in score"); + } + + //-----------------// + // getMeasureRange // + //-----------------// + /** + * Report the potential range of selected measures. + * + * @return the selected measure range, perhaps null + */ + public MeasureRange getMeasureRange () + { + return measureRange; + } + + //-------------// + // getMidiFile // + //-------------// + /** + * Report to which file, if any, the MIDI data is to be exported. + * + * @return the Midi file, or null + */ + public File getMidiFile () + { + return midiFile; + } + + //---------// + // getPage // + //---------// + /** + * Report the page with provided page-index. + * + * @param pageIndex the desired value for page index + * @return the proper page, or null if not found + */ + public Page getPage (int pageIndex) + { + for (TreeNode pn : getPages()) { + Page page = (Page) pn; + + if (page.getIndex() == pageIndex) { + return page; + } + } + + return null; + } + + //----------// + // getPages // + //----------// + /** + * Report the collection of pages in that score. + * + * @return the pages + */ + public List getPages () + { + return getChildren(); + } + + //-------------// + // isMultiPage // + //-------------// + /** + * @return the multiPage + */ + public boolean isMultiPage () + { + return multiPage; + } + + //--------// + // isIdle // + //--------// + /** + * Check whether this score is idle or not. + * The score is busy when at least one of its pages/sheets is under a step + * processing. + * + * @return true if idle, false if busy + */ + public boolean isIdle () + { + for (TreeNode pn : getPages()) { + Page page = (Page) pn; + Sheet sh = page.getSheet(); + if (sh != null && sh.getCurrentStep() != null) { + return false; + } + } + return true; + } + + //-----------------// + // setDefaultTempo // + //-----------------// + /** + * Assign default value for Midi tempo. + * + * @param tempo the default tempo value + */ + public static void setDefaultTempo (int tempo) + { + constants.defaultTempo.setValue(tempo); + } + + //------------------// + // setDefaultVolume // + //------------------// + /** + * Assign default value for Midi volume. + * + * @param volume the default volume value + */ + public static void setDefaultVolume (int volume) + { + constants.defaultVolume.setValue(volume); + } + + //----------// + // getBench // + //----------// + /** + * Report the related sheet bench. + * + * @return the related bench + */ + public ScoreBench getBench () + { + return bench; + } + + //-----------------// + // getBrowserFrame // + //-----------------// + /** + * Create a dedicated frame, where all score elements can be + * browsed in the tree hierarchy. + * + * @return the created frame + */ + public JFrame getBrowserFrame () + { + if (scoreTree == null) { + // Build the ScoreTree on the score + scoreTree = new ScoreTree(this); + } + + return scoreTree.getFrame(); + } + + //-------------// + // getLastPage // + //-------------// + public Page getLastPage () + { + if (children.isEmpty()) { + return null; + } else { + return (Page) children.get(children.size() - 1); + } + } + + //------------------// + // getMeasureOffset // + //------------------// + /** + * Report the offset to add to page-based measure index of the + * provided page to get absolute (score-based) indices. + * + * @param page the provided page + * @return the measure index offset for the page + */ + public int getMeasureOffset (Page page) + { + int offset = 0; + + for (TreeNode pn : getPages()) { + Page p = (Page) pn; + + if (p == page) { + return offset; + } else { + offset += p.getMeasureCount(); + } + } + + throw new IllegalArgumentException(page + " not found in score"); + } + + //-------------// + // getPartList // + //-------------// + /** + * Report the global list of parts. + * + * @return partList the list of score parts + */ + public List getPartList () + { + return partList; + } + + //--------------// + // getPrintFile // + //--------------// + /** + * Report to which file, if any, the sheet PDF data is to be written. + * + * @return the sheet PDF file, or null + */ + public File getPrintFile () + { + return printFile; + } + + //----------// + // getRadix // + //----------// + /** + * Report the radix of the file that corresponds to the score. + * It is based on the simple file name of the score, with no path and no + * extension. + * + * @return the score input file radix + */ + public String getRadix () + { + return radix; + } + + //--------------// + // getLogPrefix // + //--------------// + /** + * Report the proper prefix to use when logging a message + * + * @return the proper prefix + */ + public String getLogPrefix () + { + if (ScoresManager.isMultiScore()) { + return "[" + radix + "] "; + } else { + return ""; + } + } + + //-----------// + // getScript // + //-----------// + public Script getScript () + { + if (script == null) { + script = new Script(this); + } + + return script; + } + + //---------------// + // getScriptFile // + //---------------// + /** + * Report the file, if any, where the script should be written. + * + * @return the related script file or null + */ + public File getScriptFile () + { + return scriptFile; + } + + //-----------// + // getVolume // + //-----------// + /** + * Report the assigned volume, if any. + * If the value is not yet set, it is set to the default value and returned. + * + * @return the assigned volume, or null + */ + public Integer getVolume () + { + if (!hasVolume()) { + volume = getDefaultVolume(); + } + + return volume; + } + + //-------------// + // hasLanguage // + //-------------// + /** + * Check whether a language has been defined for this score. + * + * @return true if a language is defined + */ + public boolean hasLanguage () + { + return language != null; + } + + //-----------// + // hasVolume // + //-----------// + /** + * Check whether a volumehas been defined for this score. + * + * @return true if a volume is defined + */ + public boolean hasVolume () + { + return volume != null; + } + + //--------// + // remove // + //--------// + /** + * Remove a page + */ + public void remove (Page page) + { + getPages().remove(page); + setMultiPage(getPages().size() > 1); + } + + //--------------------// + // setDurationDivisor // + //--------------------// + /** + * Remember the common divisor used for this score when + * simplifying the durations. + * + * @param durationDivisor the computed divisor (GCD), or null + */ + public void setDurationDivisor (Integer durationDivisor) + { + this.durationDivisor = durationDivisor; + } + + //---------------// + // setExportFile // + //---------------// + /** + * Remember to which file the score is to be exported. + * + * @param exportFile the exported xml file + */ + public void setExportFile (File exportFile) + { + this.exportFile = exportFile; + } + + //-------------// + // setLanguage // + //-------------// + /** + * Set the score dominant language. + * + * @param language the dominant language + */ + public void setLanguage (String language) + { + this.language = language; + } + + //-----------------// + // setMeasureRange // + //-----------------// + /** + * Remember a range of measure for this score. + * + * @param measureRange the range of selected measures + */ + public void setMeasureRange (MeasureRange measureRange) + { + this.measureRange = measureRange; + } + + //-------------// + // setMidiFile // + //-------------// + /** + * Remember to which file the MIDI data is to be exported. + * + * @param midiFile the Midi file + */ + public void setMidiFile (File midiFile) + { + this.midiFile = midiFile; + } + + //-------------// + // setPartList // + //-------------// + /** + * Assign a part list valid for the whole score. + * + * @param partList the list of score parts + */ + public void setPartList (List partList) + { + this.partList = partList; + } + + //--------------// + // setPrintFile // + //--------------// + /** + * Remember to which file the sheet PDF data is to be exported. + * + * @param sheetPdfFile the sheet PDF file + */ + public void setPrintFile (File sheetPdfFile) + { + this.printFile = sheetPdfFile; + } + + //---------------// + // setScriptFile // + //---------------// + /** + * Remember the file where the script is written. + * + * @param scriptFile the related script file + */ + public void setScriptFile (File scriptFile) + { + this.scriptFile = scriptFile; + } + + //-----------// + // setVolume // + //-----------// + /** + * Assign a volume value. + * + * @param volume the volume value to be assigned + */ + public void setVolume (Integer volume) + { + this.volume = volume; + } + + //------------------// + // simpleDurationOf // + //------------------// + /** + * Export a duration to its simplest form, based on the greatest + * duration divisor of the score. + * + * @param value the raw duration + * @return the simple duration expression, in the param of proper + * divisions + */ + public int simpleDurationOf (Rational value) + { + return value.num * (getDurationDivisor() / value.den); + } + + //----------// + // toString // + //----------// + /** + * Report a readable description. + * + * @return a string based on its XML file name + */ + @Override + public String toString () + { + if (getRadix() != null) { + return "{Score " + getRadix() + "}"; + } else { + return "{Score }"; + } + } + + //--------------// + // setMultiPage // + //--------------// + /** + * @param multiPage the multiPage to set. + */ + private void setMultiPage (boolean multiPage) + { + this.multiPage = multiPage; + } + + //~ Inner Classes ---------------------------------------------------------- + //-----------// + // Constants // + //-----------// + private static final class Constants + extends ConstantSet + { + //~ Instance fields ---------------------------------------------------- + + Constant.Integer defaultTempo = new Constant.Integer( + "QuartersPerMn", + 120, + "Default tempo, stated in number of quarters per minute"); + + Constant.Integer defaultVolume = new Constant.Integer( + "Volume", + 78, + "Default Volume in 0..127 range"); + + } + + //------------// + // PartsParam // + //------------// + private class PartsParam + extends Param> + { + + @Override + public List getSpecific () + { + List list = getPartList(); + if (list != null) { + List data = new ArrayList<>(); + for (ScorePart scorePart : list) { + + // Initial setting for part midi program + int prog = (scorePart.getMidiProgram() != null) + ? scorePart.getMidiProgram() + : scorePart.getDefaultProgram(); + + data.add(new PartData(scorePart.getName(), prog)); + } + return data; + } else { + return null; + } + } + + @Override + public boolean setSpecific (List specific) + { + try { + for (int i = 0; i < specific.size(); i++) { + PartData data = specific.get(i); + ScorePart scorePart = getPartList().get(i); + + // Part name + scorePart.setName(data.name); + + // Part midi program + scorePart.setMidiProgram(data.program); + } + + logger.info("Score parts have been updated"); + + return true; + } catch (Exception ex) { + logger.warn("Error updating score parts", ex); + } + + return false; + } + } +} diff --git a/src/main/omr/score/ScoreBench.java b/src/main/omr/score/ScoreBench.java new file mode 100644 index 0000000..7ef66d7 --- /dev/null +++ b/src/main/omr/score/ScoreBench.java @@ -0,0 +1,270 @@ +//----------------------------------------------------------------------------// +// // +// S c o r e B e n c h // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.score; + +import omr.WellKnowns; + +import omr.sheet.Bench; + +import omr.step.Step; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.io.OutputStream; +import java.io.PrintWriter; +import java.util.Date; +import java.util.HashSet; +import java.util.Properties; +import java.util.Set; +import java.util.SortedSet; +import java.util.TreeSet; + +/** + * Class {@code ScoreBench} is in charge of recording all important information + * related to the processing of a music score, and producing an output formatted + * as "key = value" lines of text. + * + *

In order to cope with possible multiple recordings with the same radix, we + * always add to a temporary property set, using numbered suffixes (.01, .02, + * etc) so that no data is ever overwritten. The temporary set contains only + * lines formatted as "radix.suffix = value".

+ * + *

When the recordings are to be flushed, the temporary set is used to + * produce a clean set of external properties, according to the following + * rules:

    + * + *
  • When only the .01 suffix exists for a given radix, then the externals + * just contains the "radix = value" line, and the .01 suffix is not transferred + * to the output.
  • + * + *
  • When more than the .01 suffix exist for a given radix, then these + * intermediate "radix.suffix = value" pairs are copied to the externals as they + * are. The last key/value pair is also used to set the "radix = value" pair in + * the externals (so that the latest value is always accessible through its + * simple radix)
  • + * + *
  • For a special kind of keys (step.[name].duration), the "radix = value" + * line does not contain the latest intermediate value, but rather the sum of + * all intermediate values
+ * + *

The recorded data can be flushed to disk on specific occasions, to make + * sure that no data ever get lost even in the case of step cancellation or + * program interruption.
In case of step cancellation the line + * "whole.cancelled = true" is added to the externals.
In case of program + * interruption the line "whole.interrupted = true" is kept in the + * externals.

+ * + * @author Hervé Bitteur + */ +public class ScoreBench + extends Bench +{ + //~ Static fields/initializers --------------------------------------------- + + /** Usual logger utility */ + private static final Logger logger = LoggerFactory.getLogger(ScoreBench.class); + + /** Special key which indicates that an interruption has occurred */ + private static final String INTERRUPTION_KEY = "whole.interrupted"; + + //~ Instance fields -------------------------------------------------------- + /** The related score */ + private final Score score; + + /** Time stamp when this instance was created */ + private final long startTime = System.currentTimeMillis(); + + /** Starting date */ + private final Date date = new Date(startTime); + + //~ Constructors ----------------------------------------------------------- + //------------// + // ScoreBench // + //------------// + /** + * Creates a new ScoreBench object. + * + * @param score the related score + */ + public ScoreBench (Score score) + { + this.score = score; + + // To be later removed, but only at normal completion point + addProp(INTERRUPTION_KEY, "true"); + + addProp("date", date.toString()); + addProp("program", WellKnowns.TOOL_NAME); + addProp("version", WellKnowns.TOOL_REF); + addProp("image", score.getImagePath()); + + flushBench(); + } + + //~ Methods ---------------------------------------------------------------- + //------------// + // flushBench // + //------------// + /** + * Flush the current content of bench to disk + */ + @Override + public final synchronized void flushBench () + { + ScoresManager.getInstance() + .storeBench(this, null, false); + } + + //----------// + // getScore // + //----------// + /** + * @return the score + */ + public Score getScore () + { + return score; + } + + //--------------------// + // recordCancellation // + //--------------------// + public synchronized void recordCancellation () + { + addProp("whole.cancelled", "true"); + } + + //------------// + // recordStep // + //------------// + public synchronized void recordStep (Step step, + long duration) + { + addProp( + "step." + step.getName().toLowerCase() + ".duration", + "" + duration); + flushBench(); + } + + //-------// + // store // + //-------// + /** + * Store this bench into an output stream + * + * @param output the output stream to be written + * @param complete true if bench data must be finalized + * @throws IOException + */ + public synchronized void store (OutputStream output, + boolean complete) + throws IOException + { + // Build external properties + Properties externals = cleanupProps(); + + // What do we do with the script data? and with app constants? + // TBD + + // Insert global duration (up till now) + long wholeDuration = System.currentTimeMillis() - startTime; + externals.setProperty("whole.duration", "" + wholeDuration); + + // Finalize this bench? + if (complete) { + Object obj = externals.remove(INTERRUPTION_KEY); + } + + // Sort and store to file + SortedSet keys = new TreeSet<>(); + + for (Object obj : externals.keySet()) { + String key = (String) obj; + keys.add(key); + } + + PrintWriter writer = new PrintWriter(output); + + for (String key : keys) { + writer.println(key + " = " + externals.getProperty(key)); + } + + writer.flush(); + } + + //--------------// + // cleanupProps // + //--------------// + /** + * Build the externals properties, radix by radix, playing with the key + * suffixes + */ + private Properties cleanupProps () + { + Properties externals = new Properties(); + + // Retrieve key radices + Set radices = new HashSet<>(); + + for (Object obj : props.keySet()) { + String key = (String) obj; + int dot = key.lastIndexOf('.'); + String radix = key.substring(0, dot); + radices.add(radix); + } + + // Browse radices + for (String radix : radices) { + if (!props.containsKey(keyOf(radix, 2))) { + // We have just 1 property: we rename it as the radix + String key1 = keyOf(radix, 1); + externals.setProperty(radix, props.getProperty(key1)); + } else { + // We have several properties, so we keep all the intermediate values + // Special case for step radix: we sum up the durations + // Standard radix case: we use the latest value + boolean isStep = radix.startsWith("step.") + && radix.endsWith(".duration"); + int sum = 0; + + for (int index = 1;; index++) { + String key = keyOf(radix, index); + String str = props.getProperty(key); + + if (str == null) { + break; + } else { + // Keep intermediate value + externals.setProperty(key, str); + + if (isStep) { + // Sum up step durations + sum += Integer.parseInt(str); + } else { + // Overwrite the radix value + externals.setProperty(radix, str); + } + } + } + + if (isStep) { + // Write the total sum as the radix value + externals.setProperty(radix, "" + sum); + } + } + } + + return externals; + } +} diff --git a/src/main/omr/score/ScoreChecker.java b/src/main/omr/score/ScoreChecker.java new file mode 100644 index 0000000..61d7b90 --- /dev/null +++ b/src/main/omr/score/ScoreChecker.java @@ -0,0 +1,959 @@ +//----------------------------------------------------------------------------// +// // +// S c o r e C h e c k e r // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.score; + +import omr.constant.Constant; +import omr.constant.ConstantSet; + +import omr.glyph.Evaluation; +import omr.glyph.GlyphNetwork; +import omr.glyph.Grades; +import omr.glyph.Shape; +import omr.glyph.ShapeEvaluator; +import omr.glyph.ShapeSet; +import omr.glyph.facets.Glyph; + +import omr.math.GeoUtil; + +import omr.score.entity.Beam; +import omr.score.entity.BeamGroup; +import omr.score.entity.Chord; +import omr.score.entity.Dynamics; +import omr.score.entity.Measure; +import omr.score.entity.Note; +import omr.score.entity.ScoreSystem; +import omr.score.entity.Staff; +import omr.score.entity.SystemPart; +import omr.score.entity.TimeSignature; +import omr.score.entity.TimeSignature.InvalidTimeSignature; +import omr.score.visitor.AbstractScoreVisitor; + +import omr.sheet.Scale; +import omr.sheet.SystemInfo; + +import omr.util.Predicate; +import omr.util.TreeNode; +import omr.util.WrappedBoolean; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.awt.Point; +import java.awt.Rectangle; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.EnumMap; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** + * Class {@code ScoreChecker} can visit the score hierarchy and perform + * global checking on score nodes. + *

We use it for:

    + *
  • Improving the recognition of beam hooks
  • + *
  • Fixing false beam hooks
  • + *
  • Forcing consistency among time signatures
  • + *
  • Making sure all dynamics can be assigned a shape
  • + *
  • Merge note heads with pitches too close to each other
  • + *
  • Enforce consistency of note heads within the same chord
  • + *
+ * + * TODO: Split this class into smaller modular classes, one per feature + * since the browsing of the score by itself is very cheap (.15 ms for a page) + * + * @author Hervé Bitteur + */ +public class ScoreChecker + extends AbstractScoreVisitor +{ + //~ Static fields/initializers --------------------------------------------- + + /** Specific application parameters */ + private static final Constants constants = new Constants(); + + /** Usual logger utility */ + private static final Logger logger = LoggerFactory.getLogger(ScoreChecker.class); + + /** Specific predicate for beam hooks */ + private static final Predicate hookPredicate = new Predicate() + { + @Override + public boolean check (Shape shape) + { + return ShapeSet.Beams.contains(shape); + } + }; + + //~ Instance fields -------------------------------------------------------- + /** Glyph evaluator */ + private final ShapeEvaluator evaluator = GlyphNetwork.getInstance(); + + /** Output of the checks */ + private final WrappedBoolean modified; + + //~ Constructors ----------------------------------------------------------- + //--------------// + // ScoreChecker // + //--------------// + /** + * Creates a new ScoreChecker object. + * + * @param modified This is actually an out parameter, to tell if one or + * several entities have been modified by the score visit + */ + public ScoreChecker (WrappedBoolean modified) + { + this.modified = modified; + } + + //~ Methods ---------------------------------------------------------------- + //------------// + // visit Beam // + //------------// + /** + * Check that all beam hooks are legal + * + * @param beam the beam to check + * @return true + */ + @Override + public boolean visit (Beam beam) + { + try { + Glyph glyph = beam.getFirstItem().getGlyph(); + + if (!beam.isHook() || glyph.isManualShape()) { + return true; + } + + List chords = beam.getChords(); + + if (chords.size() > 1) { + beam.addError(glyph, "Beam hook connected to several chords"); + + return true; + } + + if (chords.isEmpty()) { + beam.addError(glyph, "Beam hook connected to no chords"); + + return true; + } + + // Check that there is at least one full beam on the same chord + // And vertically closer than the chord head + Chord chord = chords.get(0); + int stemX = chord.getStem().getLocation().x; + double hookY = glyph.getCentroid().y; + int headY = chord.getHeadLocation().y; + double toHead = Math.abs(headY - hookY); + + for (Beam b : chord.getBeams()) { + if (!b.isHook()) { + // Check hook is closer to beam than to head + double beamY = b.getLine().yAtX(stemX); + double toBeam = Math.abs(beamY - hookY); + + if (toBeam <= toHead) { + return true; + } + } + } + + // No real beam found on the same chord, so let's discard the hook + if (glyph.isVip() || logger.isDebugEnabled()) { + logger.info("{} Removing false beam hook {}", + beam.getMeasure().getContextString(), glyph.idString()); + } + + glyph.setShape(null); + modified.set(true); + + return true; + } catch (Exception ex) { + logger.warn( + getClass().getSimpleName() + " Error visiting " + beam, + ex); + } + + return true; + } + + //-------------// + // visit Chord // + //-------------// + @Override + public boolean visit (Chord chord) + { + try { + // Check note heads pitches + checkNotePitches(chord); + + // Check note heads consistency + checkNoteConsistency(chord); + + // Check void note heads WRT flags or beams + checkVoidHeads(chord); + + // Check note heads do not appear on both stem head and tail + checkHeadLocations(chord); + } catch (Exception ex) { + logger.warn( + getClass().getSimpleName() + " Error visiting " + chord, + ex); + } + + return true; + } + + //----------------// + // visit Dynamics // + //----------------// + /** + * Check that each dynamics shape can be computed + * + * @param dynamics the dynamics item + * @return true + */ + @Override + public boolean visit (Dynamics dynamics) + { + try { + dynamics.getShape(); + } catch (Exception ex) { + logger.warn( + getClass().getSimpleName() + " Error visiting " + dynamics, + ex); + } + + return true; + } + + //---------------// + // visit Measure // + //---------------// + /** + * This method is used to detect & fix unrecognized beam hooks + * + * @param measure the measure to browse + * @return true. The real output is stored in the modified global which is + * set to true if at least a beam_hook has been fixed. + */ + @Override + public boolean visit (Measure measure) + { + try { + final Scale scale = measure.getScale(); + final ScoreSystem system = measure.getSystem(); + final SystemInfo systemInfo = system.getInfo(); + + // Check the beam groups for non-recognized hooks + for (BeamGroup group : measure.getBeamGroups()) { + for (Chord chord : group.getChords()) { + Glyph stem = chord.getStem(); + + // We could have rests (w/o stem!) + if (stem != null) { + searchHooks( + chord, + systemInfo.lookupIntersectedGlyphs( + systemInfo.stemBoxOf(stem), + stem)); + } + } + } + } catch (Exception ex) { + logger.warn( + getClass().getSimpleName() + " Error visiting " + measure, + ex); + } + + return true; + } + + //-------------// + // visit Score // + //-------------// + /** + * Not used + * + * @param score + * @return true + */ + @Override + public boolean visit (Score score) + { + try { + logger.debug("Checking score ..."); + score.acceptChildren(this); + } catch (Exception ex) { + logger.warn( + getClass().getSimpleName() + " Error visiting " + score, + ex); + } + + return false; + } + + //------------------// + // visit SystemPart // + //------------------// + /** + * Check that all slurs have embraced notes on each end, except + * perhaps on left and right sides of the part + * + * @param systemPart the part to process + * @return true, since measures below must be visited too + */ + @Override + public boolean visit (SystemPart systemPart) + { + systemPart.checkSlurConnections(); + + return true; + } + + //---------------------// + // visit TimeSignature // + //---------------------// + /** + * Method used to check and correct the consistency between all time + * signatures that occur in parallel measures. + * + * @param timeSignature the score entity that triggers the check + * @return true + */ + @Override + public boolean visit (TimeSignature timeSignature) + { + try { + logger.debug("{} Checking {}", + timeSignature.getContextString(), timeSignature); + + // Trigger computation of Num & Den if not already done + Shape shape = timeSignature.getShape(); + + if (shape == null) { + // This is assumed to be a complex time sig + // (with no equivalent predefined shape) + // Just check we are able to get num and den + if ((timeSignature.getNumerator() == null) + || (timeSignature.getDenominator() == null)) { + timeSignature.addError( + "Time signature with no rational value"); + } else { + logger.debug("Complex {}", timeSignature); + + // Normal complex shape + if (!timeSignature.isDummy()) { + checkTimeSig(timeSignature); + } + } + } else if (shape == Shape.NO_LEGAL_TIME) { + timeSignature.addError("Illegal " + timeSignature); + } else if (ShapeSet.PartialTimes.contains(shape)) { + // This time sig has the shape of a single digit + // So some other part is still missing + timeSignature.addError( + "Orphan time signature shape : " + shape); + } else { + // Normal predefined shape + if (!timeSignature.isDummy()) { + checkTimeSig(timeSignature); + } + } + } catch (Exception ex) { + logger.warn( + getClass().getSimpleName() + " Error visiting " + + timeSignature, + ex); + } + + return true; + } + + //- Utilities -------------------------------------------------------------- + //------------------// + // arePitchDeltasOk // + //------------------// + private boolean arePitchDeltasOk (List list) + { + double minDeltaPitch = constants.minDeltaNotePitch.getValue(); + Note lastNote = null; + + for (Note note : list) { + if (lastNote != null) { + double deltaPitch = note.getPitchPosition() + - lastNote.getPitchPosition(); + + if (Math.abs(deltaPitch) < minDeltaPitch) { + logger.debug("Too small delta pitch between {} & {}", + note, lastNote); + mergeNotes(lastNote, note); + + return false; + } + } + + lastNote = note; + } + + return true; + } + + //----------------------// + // checkNoteConsistency // + //----------------------// + /** + * Check that all note heads of a chord are of the same shape + * (either all black or all void). + * + * @param chord + */ + private void checkNoteConsistency (Chord chord) + { + EnumMap> shapes = new EnumMap<>(Shape.class); + + for (TreeNode node : chord.getNotes()) { + Note note = (Note) node; + + if (!note.isRest()) { + Shape shape = note.getShape(); + List notes = shapes.get(shape); + + if (notes == null) { + notes = new ArrayList<>(); + shapes.put(shape, notes); + } + + notes.add(note); + } + } + + if (shapes.keySet().size() > 1) { + chord.addError(chord.getStem(), + "Note inconsistency in " + chord + shapes); + + // Check evaluations + double bestEval = Double.MIN_VALUE; + Shape bestShape = null; + + for (Shape shape : shapes.keySet()) { + List notes = shapes.get(shape); + + for (Note note : notes) { + for (Glyph glyph : note.getGlyphs()) { + if (glyph.getGrade() > bestEval) { + bestEval = glyph.getGrade(); + bestShape = shape; + } + } + } + } + + logger.debug("{} aligned on shape {}", chord, bestShape); + + final Shape baseShape = bestShape; // Must be final + Predicate predicate = new Predicate() + { + final Collection desiredShapes = Arrays.asList( + Note.getActualShape(baseShape, 1), + Note.getActualShape(baseShape, 2), + Note.getActualShape(baseShape, 3)); + + @Override + public boolean check (Shape shape) + { + return desiredShapes.contains(shape); + } + }; + + ScoreSystem system = chord.getSystem(); + + for (Shape shape : shapes.keySet()) { + if (shape == bestShape) { + continue; + } + + List notes = shapes.get(shape); + + for (Note note : notes) { + for (Glyph glyph : note.getGlyphs()) { + Evaluation vote = evaluator.vote( + glyph, + system.getInfo(), + Grades.consistentNoteMinGrade, + predicate); + + if (vote != null) { + glyph.setEvaluation(vote); + } + } + } + } + } + } + + //------------------// + // checkNotePitches // + //------------------// + /** + * Check that on each side of the chord stem, the notes pitches are + * not too close to each other. + * + * @param chord the chord at hand + */ + private void checkNotePitches (Chord chord) + { + Glyph stem = chord.getStem(); + + if (stem == null) { + return; + } + + Point pixPoint = stem.getAreaCenter(); + Point stemCenter = pixPoint; + + // Look on left and right sides + List allNotes = new ArrayList<>(chord.getNotes()); + Collections.sort(allNotes, Chord.noteHeadComparator); + + List lefts = new ArrayList<>(); + List rights = new ArrayList<>(); + + for (TreeNode nNode : allNotes) { + Note note = (Note) nNode; + Point center = note.getCenter(); + + if (center.x < stemCenter.x) { + lefts.add(note); + } else { + rights.add(note); + } + } + + // Check on left & right + if (!arePitchDeltasOk(lefts)) { + modified.set(true); + } + + if (!arePitchDeltasOk(rights)) { + modified.set(true); + } + } + + //--------------// + // checkTimeSig // + //--------------// + /** + * Here we check time signature across all staves of the system + * + * @param timeSignature the sig to check + */ + private void checkTimeSig (TimeSignature timeSignature) + { + // Check others, similar abscissa, in all other staves of the system + // Use score hierarchy, same system, all parts, same measure id + // If there is no time sig, create a dummy one + // If there is one, make sure the sig is identical + // Priority to manually assigned shapes of course + TimeSignature bestSig = findBestTimeSig(timeSignature.getMeasure()); + + if (bestSig != null) { + for (Staff.SystemIterator sit = new Staff.SystemIterator( + timeSignature.getMeasure()); sit.hasNext();) { + Staff staff = sit.next(); + Measure measure = sit.getMeasure(); + TimeSignature sig = measure.getTimeSignature(staff); + + if (sig == null) { + sig = new TimeSignature(measure, staff, bestSig); + + try { + logger.debug("{} Created time sig {}/{}", + sig.getContextString(), + sig.getNumerator(), sig.getDenominator()); + } catch (InvalidTimeSignature ignored) { + logger.warn("InvalidTimeSignature", ignored); + } + } else { + logger.debug("{} Existing sig {}", + sig.getContextString(), sig); + } + } + } + } + + //----------------// + // checkVoidHeads // + //----------------// + /** + * Check that void note heads do not coexist with flags or beams. + * + * @param chord + */ + private void checkVoidHeads (Chord chord) + { + // Void heads? + double noteGrade = Double.MIN_VALUE; + Set voidGlyphs = new HashSet<>(); + + for (TreeNode node : chord.getNotes()) { + Note note = (Note) node; + Shape noteShape = note.getShape(); + + if (ShapeSet.VoidNoteHeads.contains(noteShape)) { + for (Glyph glyph : note.getGlyphs()) { + noteGrade = Math.max(noteGrade, glyph.getGrade()); + voidGlyphs.add(glyph); + } + } + } + + if (voidGlyphs.isEmpty()) { + return; + } + + Predicate blackHeadPredicate = new Predicate() + { + @Override + public boolean check (Shape shape) + { + return ShapeSet.BlackNoteHeads.contains(shape); + } + }; + + // Flags or beams + boolean fix = false; + + if (!chord.getBeams().isEmpty()) { + // We trust beams + logger.debug("{} Head/beam conflict in {}", + chord.getContextString(), chord); + fix = true; + } else if (chord.getFlagsNumber() > 0) { + // Check grade of flag(s) + double flagGrade = Double.MIN_VALUE; + + for (Glyph flag : chord.getFlagGlyphs()) { + flagGrade = Math.max(flagGrade, flag.getGrade()); + } + + logger.debug("{} Head/flag conflict in {}", + chord.getContextString(), chord); + + if (noteGrade <= flagGrade) { + fix = true; + } + } + + if (fix) { + // Change note shape (void -> black) + for (Glyph glyph : voidGlyphs) { + Evaluation vote = evaluator.rawVote( + glyph, + Grades.consistentNoteMinGrade, + blackHeadPredicate); + + if (vote != null) { + glyph.setEvaluation(vote); + } + } + } + } + + //--------------------// + // checkHeadLocations // + //--------------------// + /** + * Check that note heads do not appear on both stem head and tail. + * On tail we can have nothing or beams or flags, but no heads + * + * @param chord the chord to check + */ + private void checkHeadLocations (Chord chord) + { + // This test applies only to chords with stem + Glyph stem = chord.getStem(); + if (stem == null) { + return; + } + + Rectangle tailBox = new Rectangle(chord.getTailLocation()); + int halfTailBoxSide = chord.getScale().toPixels(constants.halfTailBoxSide); + tailBox.grow(halfTailBoxSide, halfTailBoxSide); + + for (TreeNode node : chord.getNotes()) { + Note note = (Note) node; + + // If note is close to tail, it can't be a note + if (note.getBox().intersects(tailBox)) { + for (Glyph glyph : note.getGlyphs()) { + if (logger.isDebugEnabled() || glyph.isVip()) { + logger.info("Note {} too close to tail of stem {}", + note, stem); + } + glyph.setShape(null); + } + modified.set(true); + } + } + } + + //-----------------// + // findBestTimeSig // + //-----------------// + /** + * Report the best time signature for all parallel measures + * (among all the parallel candidate time signatures) + * + * @param measure the reference measure + * @return the best signature, or null if no suitable signature found + */ + private TimeSignature findBestTimeSig (Measure measure) + { + TimeSignature manualSig; + + try { + manualSig = findManualTimeSig(measure); // Perhaps null + } catch (Exception ex) { + measure.addError("Inconsistent Measures or TimeSignatures"); + + return null; + } + + TimeSignature bestSig = manualSig; + + for (Staff.SystemIterator sit = new Staff.SystemIterator(measure); + sit.hasNext();) { + Staff staff = sit.next(); + measure = sit.getMeasure(); + + TimeSignature sig = measure.getTimeSignature(staff); + + if ((sig == null) || sig.isDummy()) { + continue; + } + + try { + // Make sure the signature is valid + int num = sig.getNumerator(); + int den = sig.getDenominator(); + + // First instance? + if (bestSig == null) { + bestSig = sig; + + continue; + } + + // Still consistent? + if ((num == bestSig.getNumerator()) + && (den == bestSig.getDenominator()) + && sig.getShape() == bestSig.getShape()) { + continue; + } + + // Inconsistency detected + if (manualSig != null) { + // Assign this manual sig to this (different) sig + sig.copy(manualSig); + } else { + // Inconsistent sigs + logger.debug("Inconsistency between time sigs"); + sig.addError("Inconsistent time signature "); + bestSig.addError("Inconsistent time signature"); + + return null; + } + } catch (Exception ex) { + // Skip invalid signatures + } + } + + return bestSig; + } + + //-------------------// + // findManualTimeSig // + //-------------------// + /** + * Report a suitable manually assigned time signature, if any. + * For this, we need to find a manual time sig, after having checked that + * all manual time sigs in the measure are consistent. + * + * @param measure the reference measure + * @return the suitable manual sig, if any + * @throws InconsistentTimeSignatures if at least two manual sigs differ + */ + private TimeSignature findManualTimeSig (Measure measure) + throws InconsistentTimeSignatures + { + TimeSignature manualSig = null; + + for (Staff.SystemIterator sit = new Staff.SystemIterator(measure); + sit.hasNext();) { + Staff staff = sit.next(); + TimeSignature sig = sit.getMeasure().getTimeSignature(staff); + + if ((sig != null) && !sig.isDummy() && sig.isManual()) { + try { + // Make sure the signature is valid + int num = sig.getNumerator(); + int den = sig.getDenominator(); + + // First instance? + if (manualSig == null) { + manualSig = sig; + + continue; + } + + // Still consistent? + if ((num != manualSig.getNumerator()) + || (den != manualSig.getDenominator())) { + sig.addError("Inconsistent time signature"); + manualSig.addError("Inconsistent time signature"); + + throw new InconsistentTimeSignatures(); + } + } catch (InvalidTimeSignature | InconsistentTimeSignatures ex) { + // Unusable signature, forget about this one + } + } + } + + return manualSig; + } + + //------------// + // mergeNotes // + //------------// + private void mergeNotes (Note first, + Note second) + { + if ((first.getShape() == Shape.NOTEHEAD_VOID) + && (second.getShape() == Shape.NOTEHEAD_VOID)) { + List glyphs = new ArrayList<>(); + + glyphs.addAll(first.getGlyphs()); + glyphs.addAll(second.getGlyphs()); + + SystemInfo system = first.getSystem().getInfo(); + Glyph compound = system.buildTransientCompound(glyphs); + Evaluation vote = GlyphNetwork.getInstance().vote( + compound, + first.getSystem().getInfo(), + Grades.mergedNoteMinGrade); + + if (vote != null) { + compound = system.addGlyph(compound); + compound.setEvaluation(vote); + logger.debug("{} merged two note heads", compound.idString()); + } + } + } + + //-------------// + // searchHooks // + //-------------// + /** + * Search unrecognized beam hooks among the glyphs around the + * provided chord. + * + * @param chord the provided chord + * @param glyphs the surrounding glyphs + */ + private void searchHooks (Chord chord, + Collection glyphs) + { + // Up(+1) or down(-1) stem? + final int stemDir = chord.getStemDir(); + final Point chordCenter = chord.getCenter(); + final ScoreSystem system = chord.getSystem(); + final GlyphNetwork network = GlyphNetwork.getInstance(); + + for (Glyph glyph : glyphs) { + if (glyph.getShape() != null) { + continue; + } + + logger.debug("Spurious {}", glyph.idString()); + + // Check we are on the tail (beam) end of the stem + // Beware, stemDir is >0 upwards, while y is >0 downwards + Point glyphCenter = glyph.getAreaCenter(); + + if ((GeoUtil.vectorOf(chordCenter, glyphCenter).y * stemDir) > 0) { + logger.debug("{} not on beam side", glyph.idString()); + + continue; + } + + // Check if a beam appears in the top evaluations + Evaluation vote = network.vote( + glyph, + system.getInfo(), + Grades.hookMinGrade, + hookPredicate); + + if (vote != null) { + glyph.setShape(vote.shape, Evaluation.ALGORITHM); + + logger.debug("{} recognized as {}", + glyph.idString(), vote.shape); + + modified.set(true); + } + } + } + + //~ Inner Classes ---------------------------------------------------------- + //----------------------------// + // InconsistentTimeSignatures // + //----------------------------// + /** + * Used to signal that parallel time signatures are not consistent + */ + public static class InconsistentTimeSignatures + extends Exception + { + //~ Constructors ------------------------------------------------------- + + public InconsistentTimeSignatures () + { + super("Time signatures are inconsistent"); + } + } + + //-----------// + // Constants // + //-----------// + private static final class Constants + extends ConstantSet + { + //~ Instance fields ---------------------------------------------------- + + Constant.Double minDeltaNotePitch = new Constant.Double( + "PitchPosition", + 1.5, + "Minimum pitch difference between note heads on same stem side"); + + Scale.Fraction halfTailBoxSide = new Scale.Fraction( + 1, + "Half side of box on stem tail to exclude notes"); + + } +} diff --git a/src/main/omr/score/ScoreCleaner.java b/src/main/omr/score/ScoreCleaner.java new file mode 100644 index 0000000..0eee842 --- /dev/null +++ b/src/main/omr/score/ScoreCleaner.java @@ -0,0 +1,124 @@ +//----------------------------------------------------------------------------// +// // +// S c o r e C l e a n e r // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.score; + +import omr.glyph.Shape; +import omr.glyph.facets.Glyph; + +import omr.score.entity.Measure; +import omr.score.entity.ScoreSystem; +import omr.score.entity.SystemPart; +import omr.score.visitor.AbstractScoreVisitor; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Class {@code ScoreCleaner} can visit the score hierarchy to get rid + * of all measure items except barlines, ready for a new score + * translation. + * + * @author Hervé Bitteur + */ +public class ScoreCleaner + extends AbstractScoreVisitor +{ + //~ Static fields/initializers --------------------------------------------- + + /** Usual logger utility */ + private static final Logger logger = LoggerFactory.getLogger( + ScoreCleaner.class); + + //~ Constructors ----------------------------------------------------------- + //--------------// + // ScoreCleaner // + //--------------// + /** + * Creates a new ScoreCleaner object. + */ + public ScoreCleaner () + { + } + + //~ Methods ---------------------------------------------------------------- + //--------------// + // visit System // + //--------------// + @Override + public boolean visit (ScoreSystem system) + { + try { + logger.debug("Cleaning up {}", system); + + // Remove recorded translations for all system glyphs + for (Glyph glyph : system.getInfo() + .getGlyphs()) { + if (glyph.getShape() != Shape.LEDGER) { + glyph.clearTranslations(); + } + } + + system.acceptChildren(this); + } catch (Exception ex) { + logger.warn( + getClass().getSimpleName() + " Error visiting " + system, + ex); + } + + return false; + } + + //------------------// + // visit SystemPart // + //------------------// + @Override + public boolean visit (SystemPart systemPart) + { + try { + if (systemPart.isDummy()) { + systemPart.getParent() + .getChildren() + .remove(systemPart); + + return false; + } else { + // Remove slurs and wedges + systemPart.cleanupNode(); + + return true; + } + } catch (Exception ex) { + logger.warn( + getClass().getSimpleName() + " Error visiting " + systemPart, + ex); + } + + return false; + } + + //---------------// + // visit Measure // + //---------------// + @Override + public boolean visit (Measure measure) + { + try { + measure.cleanupNode(); + } catch (Exception ex) { + logger.warn( + getClass().getSimpleName() + " Error visiting " + measure, + ex); + } + + return false; + } +} diff --git a/src/main/omr/score/ScoreExporter.java b/src/main/omr/score/ScoreExporter.java new file mode 100644 index 0000000..d8586a1 --- /dev/null +++ b/src/main/omr/score/ScoreExporter.java @@ -0,0 +1,2996 @@ +//----------------------------------------------------------------------------// +// // +// S c o r e E x p o r t e r // +// // +//----------------------------------------------------------------------------// +// // +// 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. // +//----------------------------------------------------------------------------// +// +package omr.score; + +import omr.WellKnowns; + +import omr.constant.Constant; +import omr.constant.ConstantSet; + +import omr.glyph.Shape; +import static omr.glyph.Shape.*; + +import omr.math.Rational; +import static omr.score.MusicXML.*; +import omr.score.entity.Arpeggiate; +import omr.score.entity.Articulation; +import omr.score.entity.Barline; +import omr.score.entity.Beam; +import omr.score.entity.Chord; +import omr.score.entity.ChordSymbol; +import omr.score.entity.Clef; +import omr.score.entity.Coda; +import omr.score.entity.DirectionStatement; +import omr.score.entity.Dynamics; +import omr.score.entity.Fermata; +import omr.score.entity.KeySignature; +import omr.score.entity.LyricsItem; +import omr.score.entity.Measure; +import omr.score.entity.MeasureId.MeasureRange; +import omr.score.entity.Notation; +import omr.score.entity.Ornament; +import omr.score.entity.Page; +import omr.score.entity.Pedal; +import omr.score.entity.ScorePart; +import omr.score.entity.ScoreSystem; +import omr.score.entity.Segno; +import omr.score.entity.Slot; +import omr.score.entity.Slur; +import omr.score.entity.Staff; +import omr.score.entity.SystemPart; +import omr.score.entity.Text; +import omr.score.entity.Text.CreatorText.CreatorType; +import omr.score.entity.TimeSignature; +import omr.score.entity.TimeSignature.InvalidTimeSignature; +import omr.score.entity.Tuplet; +import omr.score.entity.Voice; +import omr.score.entity.Voice.VoiceChord; +import omr.score.entity.Wedge; +import omr.score.midi.MidiAbstractions; +import omr.score.visitor.AbstractScoreVisitor; + +import omr.sheet.Scale; + +import omr.text.FontInfo; + +import omr.util.OmrExecutors; +import omr.util.TreeNode; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.w3c.dom.Node; + +import com.audiveris.proxymusic.AboveBelow; +import com.audiveris.proxymusic.Accidental; +import com.audiveris.proxymusic.Articulations; +import com.audiveris.proxymusic.Attributes; +import com.audiveris.proxymusic.Backup; +import com.audiveris.proxymusic.BackwardForward; +import com.audiveris.proxymusic.BarStyle; +import com.audiveris.proxymusic.Bass; +import com.audiveris.proxymusic.BassAlter; +import com.audiveris.proxymusic.BassStep; +import com.audiveris.proxymusic.BeamValue; +import com.audiveris.proxymusic.ClefSign; +import com.audiveris.proxymusic.Credit; +import com.audiveris.proxymusic.Defaults; +import com.audiveris.proxymusic.Degree; +import com.audiveris.proxymusic.DegreeAlter; +import com.audiveris.proxymusic.DegreeType; +import com.audiveris.proxymusic.DegreeValue; +import com.audiveris.proxymusic.Direction; +import com.audiveris.proxymusic.DirectionType; +import com.audiveris.proxymusic.Empty; +import com.audiveris.proxymusic.EmptyPrintStyleAlign; +import com.audiveris.proxymusic.Encoding; +import com.audiveris.proxymusic.FontStyle; +import com.audiveris.proxymusic.FontWeight; +import com.audiveris.proxymusic.FormattedText; +import com.audiveris.proxymusic.Forward; +import com.audiveris.proxymusic.Harmony; +import com.audiveris.proxymusic.Identification; +import com.audiveris.proxymusic.Key; +import com.audiveris.proxymusic.Kind; +import com.audiveris.proxymusic.Lyric; +import com.audiveris.proxymusic.LyricFont; +import com.audiveris.proxymusic.MarginType; +import com.audiveris.proxymusic.MeasureNumberingValue; +import com.audiveris.proxymusic.MidiInstrument; +import com.audiveris.proxymusic.Notations; +import com.audiveris.proxymusic.NoteType; +import com.audiveris.proxymusic.Notehead; +import com.audiveris.proxymusic.NoteheadValue; +import com.audiveris.proxymusic.Ornaments; +import com.audiveris.proxymusic.OverUnder; +import com.audiveris.proxymusic.PageLayout; +import com.audiveris.proxymusic.PageMargins; +import com.audiveris.proxymusic.PartList; +import com.audiveris.proxymusic.PartName; +import com.audiveris.proxymusic.Pitch; +import com.audiveris.proxymusic.Repeat; +import com.audiveris.proxymusic.Rest; +import com.audiveris.proxymusic.RightLeftMiddle; +import com.audiveris.proxymusic.Root; +import com.audiveris.proxymusic.RootStep; +import com.audiveris.proxymusic.RootAlter; +import com.audiveris.proxymusic.Scaling; +import com.audiveris.proxymusic.ScoreInstrument; +import com.audiveris.proxymusic.ScorePartwise; +import com.audiveris.proxymusic.Sound; +import com.audiveris.proxymusic.StaffDetails; +import com.audiveris.proxymusic.StaffLayout; +import com.audiveris.proxymusic.StartStop; +import com.audiveris.proxymusic.StartStopChangeContinue; +import com.audiveris.proxymusic.StartStopContinue; +import com.audiveris.proxymusic.Stem; +import com.audiveris.proxymusic.StemValue; +import com.audiveris.proxymusic.SystemLayout; +import com.audiveris.proxymusic.SystemMargins; +import com.audiveris.proxymusic.TextElementData; +import com.audiveris.proxymusic.Tie; +import com.audiveris.proxymusic.Tied; +import com.audiveris.proxymusic.Time; +import com.audiveris.proxymusic.TimeModification; +import com.audiveris.proxymusic.TimeSymbol; +import com.audiveris.proxymusic.TypedText; +import com.audiveris.proxymusic.UprightInverted; +import com.audiveris.proxymusic.WedgeType; +import com.audiveris.proxymusic.Work; +import com.audiveris.proxymusic.YesNo; + +import com.audiveris.proxymusic.util.Marshalling; + +import java.awt.Font; +import java.awt.Point; +import java.awt.Rectangle; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.lang.reflect.Method; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.ListIterator; +import java.util.Map; +import java.util.Map.Entry; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; + +import javax.xml.bind.JAXBElement; +import javax.xml.bind.JAXBException; + +/** + * Class {@code ScoreExporter} visits the score hierarchy to export + * the score to a MusicXML file, stream or DOM. + * + * @author Hervé Bitteur + */ +public class ScoreExporter + extends AbstractScoreVisitor +{ + //~ Static fields/initializers --------------------------------------------- + + /** Specific application parameters */ + private static final Constants constants = new Constants(); + + /** Usual logger utility */ + private static final Logger logger = LoggerFactory.getLogger(ScoreExporter.class); + + /** A future which reflects whether JAXB has been initialized * */ + private static final Future loading = OmrExecutors. + getCachedLowExecutor().submit( + new Callable() + { + @Override + public Void call () + throws Exception + { + try { + Marshalling.getContext(); + } catch (JAXBException ex) { + logger.warn("Error preloading JaxbContext", ex); + throw ex; + } + + return null; + } + }); + + /** Default page horizontal margin */ + private static final BigDecimal pageHorizontalMargin = + new BigDecimal(constants.pageHorizontalMargin.getValue()); + + /** Default page vertical margin */ + private static final BigDecimal pageVerticalMargin = + new BigDecimal(constants.pageVerticalMargin.getValue()); + + //~ Instance fields -------------------------------------------------------- + /** The related score */ + private final Score score; + + /** The score proxy built precisely for export via JAXB */ + private final ScorePartwise scorePartwise = new ScorePartwise(); + + /** Current context */ + private Current current = new Current(); + + /** Current flags */ + private IsFirst isFirst = new IsFirst(); + + /** Map of Slur numbers, reset for every scorePart */ + private Map slurNumbers = new HashMap<>(); + + /** Map of Tuplet numbers, reset for every measure */ + private Map tupletNumbers = new HashMap<>(); + + /** Potential range of selected measures */ + private MeasureRange measureRange; + + /** Factory for proxymusic entities */ + private final com.audiveris.proxymusic.ObjectFactory factory = new com.audiveris.proxymusic.ObjectFactory(); + + //~ Constructors ----------------------------------------------------------- + //---------------// + // ScoreExporter // + //---------------// + /** + * Create a new ScoreExporter object, on a related score instance. + * + * @param score the score to export (cannot be null) + * @throws InterruptedException + * @throws ExecutionException + */ + public ScoreExporter (Score score) + throws InterruptedException, ExecutionException + { + if (score == null) { + throw new IllegalArgumentException("Trying to export a null score"); + } + + // Make sure the JAXB context is ready + loading.get(); + + this.score = score; + } + + //~ Methods ---------------------------------------------------------------- + // + //--------// + // export // + //--------// + /** + * Export the score to a file. + * + * @param xmlFile the xml file to write (cannot be null) + * @param injectSignature should we inject out signature? + */ + public void export (File xmlFile, + boolean injectSignature) + throws Exception + { + export(new FileOutputStream(xmlFile), injectSignature); + } + + //--------// + // export // + //--------// + /** + * Export the score to an output stream. + * + * @param os the output stream where XML data is written + * (cannot be null) + * @param injectSignature should we inject our signature? + * @throws IOException + * @throws Exception + */ + public void export (OutputStream os, + boolean injectSignature) + throws IOException, Exception + { + if (os == null) { + throw new IllegalArgumentException( + "Trying to export a score to a null output stream"); + } + + // Let visited nodes fill the scorePartWise proxy + try { + score.accept(this); + } finally { + // Marshal the proxy with what we've got + Marshalling.marshal(scorePartwise, os, injectSignature); + } + } + + //--------// + // export // + //--------// + /** + * Export the score to DOM node. + * (No longer used, it was meant for Audiveris->Zong pure java transfer) + * + * @param node the DOM node to export to (cannot be null) + * @param injectSignature should we inject our signature? + * @throws java.io.IOException + * @throws java.lang.Exception + */ + public void export (Node node, + boolean injectSignature) + throws IOException, Exception + { + if (node == null) { + throw new IllegalArgumentException( + "Trying to export a score to a null DOM Node"); + } + + try { + // Let visited nodes fill the scorePartwise proxy + buildScorePartwise(); + } finally { + // Finally, marshal the proxy with what we've got + Marshalling.marshal(scorePartwise, node, injectSignature); + } + } + + //---------// + // preload // + //---------// + /** + * Empty static method, just to trigger class elaboration. + */ + public static void preload () + { + } + + //-----------------// + // setMeasureRange // + //-----------------// + /** + * Set a specific range of measures to export. + * + * @param measureRange the range of desired measures + */ + public void setMeasureRange (MeasureRange measureRange) + { + this.measureRange = measureRange; + } + + //- All Visiting Methods --------------------------------------------------- + //------------------// + // visit Arpeggiate // + //------------------// + @Override + public boolean visit (Arpeggiate arpeggiate) + { + try { + logger.debug("Visiting {}", arpeggiate); + + com.audiveris.proxymusic.Arpeggiate pmArpeggiate = factory.createArpeggiate(); + + // relative-x + pmArpeggiate.setRelativeX( + toTenths( + arpeggiate.getReferencePoint().x + - current.note.getCenterLeft().x)); + + // number ??? + // TODO + // + getNotations().getTiedOrSlurOrTuplet().add(pmArpeggiate); + } catch (Exception ex) { + logger.warn("Error visiting " + arpeggiate, ex); + } + + return false; + } + + //--------------------// + // visit Articulation // + //--------------------// + @Override + public boolean visit (Articulation articulation) + { + try { + logger.debug("Visiting {}", articulation); + + JAXBElement element = getArticulationObject( + articulation.getShape()); + + // Staff ? + Staff staff = current.note.getStaff(); + + // Placement + Class classe = element.getDeclaredType(); + + Method method = classe.getMethod( + "setPlacement", + AboveBelow.class); + method.invoke( + element.getValue(), + (articulation.getReferencePoint().y < current.note. + getCenter().y) + ? AboveBelow.ABOVE : AboveBelow.BELOW); + + // Default-Y + method = classe.getMethod("setDefaultY", BigDecimal.class); + method.invoke( + element.getValue(), + yOf(articulation.getReferencePoint(), staff)); + + // Include in Articulations + getArticulations().getAccentOrStrongAccentOrStaccato().add(element); + } catch (Exception ex) { + logger.warn("Error visiting " + articulation, ex); + } + + return false; + } + + //---------------// + // visit Barline // + //---------------// + @Override + public boolean visit (Barline barline) + { + try { + if (barline == null) { + return false; + } + logger.debug("Visiting {}", barline); + + Shape shape = barline.getShape(); + + if ((shape != omr.glyph.Shape.THIN_BARLINE) + && (shape != omr.glyph.Shape.PART_DEFINING_BARLINE)) { + try { + com.audiveris.proxymusic.Barline pmBarline = factory.createBarline(); + com.audiveris.proxymusic.BarStyleColor barStyleColor = factory. + createBarStyleColor(); + + if (barline == current.measure.getBarline()) { + // The bar is on right side + pmBarline.setLocation(RightLeftMiddle.RIGHT); + + if ((shape == RIGHT_REPEAT_SIGN) + || (shape == BACK_TO_BACK_REPEAT_SIGN)) { + barStyleColor.setValue(BarStyle.LIGHT_HEAVY); + + Repeat repeat = factory.createRepeat(); + repeat.setDirection(BackwardForward.BACKWARD); + pmBarline.setRepeat(repeat); + } + } else { + // Inside barline (on left) + // Or bar is on left side + pmBarline.setLocation(RightLeftMiddle.LEFT); + + if ((shape == LEFT_REPEAT_SIGN) + || (shape == BACK_TO_BACK_REPEAT_SIGN)) { + barStyleColor.setValue(BarStyle.HEAVY_LIGHT); + + Repeat repeat = factory.createRepeat(); + repeat.setDirection(BackwardForward.FORWARD); + pmBarline.setRepeat(repeat); + } + } + + // Default: use style inferred from shape + // TODO: improve error handling here !!!!!!!!! + if (barStyleColor.getValue() == null) { + if (barline.getShape() != null) { + barStyleColor.setValue( + barStyleOf(barline.getShape())); + } + } + + // Everything is now OK + pmBarline.setBarStyle(barStyleColor); + current.pmMeasure.getNoteOrBackupOrForward().add(pmBarline); + } catch (Exception ex) { + logger.warn("Cannot visit barline", ex); + } + } + } catch (Exception ex) { + logger.warn("Error visiting " + barline, ex); + } + + return true; + } + + //-------------// + // visit Chord // + //-------------// + @Override + public boolean visit (Chord chord) + { + logger.error("Chord objects should not be visited by ScoreExporter"); + + return false; + } + + //------------// + // visit Clef // + //------------// + @Override + public boolean visit (Clef clef) + { + try { + logger.debug("Visiting {}", clef); + + if (isNewClef(clef)) { + getAttributes().getClef().add(buildClef(clef)); + } + } catch (Exception ex) { + logger.warn("Error visiting " + clef, ex); + } + + return true; + } + + //------------// + // visit Coda // + //------------// + @Override + public boolean visit (Coda coda) + { + try { + logger.debug("Visiting {}", coda); + + Direction direction = factory.createDirection(); + + // Staff ? + Staff staff = current.note.getStaff(); + insertStaffId(direction, staff); + + com.audiveris.proxymusic.EmptyPrintStyleAlign pmCoda = factory.createEmptyPrintStyleAlign(); + // default-x + pmCoda.setDefaultX( + toTenths( + coda.getReferencePoint().x - current.measure.getLeftX())); + + // default-y + pmCoda.setDefaultY(yOf(coda.getReferencePoint(), staff)); + + DirectionType directionType = new DirectionType(); + directionType.getCoda().add(pmCoda); + direction.getDirectionType().add(directionType); + + // Need also a Sound element + Sound sound = factory.createSound(); + direction.setSound(sound); + sound.setCoda("" + current.measure.getScoreId()); + sound.setDivisions( + new BigDecimal( + score.simpleDurationOf( + omr.score.entity.Note.QUARTER_DURATION))); + + // Everything is now OK + current.pmMeasure.getNoteOrBackupOrForward().add(direction); + } catch (Exception ex) { + logger.warn("Error visiting " + coda, ex); + } + + return true; + } + + //--------------------------// + // visit DirectionStatement // + //--------------------------// + @Override + public boolean visit (DirectionStatement words) + { + try { + logger.debug("Visiting {}", words); + + String content = words.getText().getContent(); + + if (content != null) { + Direction direction = factory.createDirection(); + DirectionType directionType = factory.createDirectionType(); + FormattedText pmWords = factory.createFormattedText(); + + pmWords.setValue(content); + + // Staff + Staff staff = current.note.getStaff(); + insertStaffId(direction, staff); + + // Placement + direction. + setPlacement( + (words.getReferencePoint().y < current.note.getCenter().y) + ? AboveBelow.ABOVE : AboveBelow.BELOW); + + // default-y + pmWords.setDefaultY(yOf(words.getReferencePoint(), staff)); + + // Font information + setFontInfo(pmWords, words.getText()); + + // relative-x + pmWords.setRelativeX( + toTenths( + words.getReferencePoint().x + - current.note.getCenterLeft().x)); + + // Everything is now OK + directionType.getWords().add(pmWords); + direction.getDirectionType().add(directionType); + current.pmMeasure.getNoteOrBackupOrForward().add(direction); + } + } catch (Exception ex) { + logger.warn("Error visiting " + words, ex); + } + + return true; + } + + //-------------------// + // visit ChordSymbol // + //-------------------// + @Override + public boolean visit (ChordSymbol symbol) + { + try { + logger.debug("Visiting {}", symbol); + + omr.score.entity.ChordInfo info = symbol.getInfo(); + Staff staff = current.note.getStaff(); + Harmony harmony = factory.createHarmony(); + + // default-y + harmony.setDefaultY(yOf(symbol.getReferencePoint(), staff)); + + // font-size + harmony.setFontSize("" + symbol.getText().getExportedFontSize()); + + // relative-x + harmony.setRelativeX( + toTenths( + symbol.getReferencePoint().x + - current.note.getCenterLeft().x)); + + // Placement + harmony.setPlacement( + (symbol.getReferencePoint().y < current.note.getCenter().y) + ? AboveBelow.ABOVE : AboveBelow.BELOW); + + // Staff + insertStaffId(harmony, staff); + + // Root + Root root = factory.createRoot(); + RootStep rootStep = factory.createRootStep(); + rootStep.setValue(stepOf(info.getRoot().step)); + root.setRootStep(rootStep); + + if (info.getRoot().alter != 0) { + RootAlter alter = factory.createRootAlter(); + alter.setValue(new BigDecimal(info.getRoot().alter)); + root.setRootAlter(alter); + } + harmony.getHarmonyChord().add(root); + + // Kind + Kind kind = factory.createKind(); + kind.setValue(kindOf(info.getKind().type)); + kind.setText(info.getKind().text); + if (info.getKind().paren) { + kind.setParenthesesDegrees(YesNo.YES); + } + if (info.getKind().symbol) { + kind.setUseSymbols(YesNo.YES); + } + harmony.getHarmonyChord().add(kind); + + // Bass + if (info.getBass() != null) { + Bass bass = factory.createBass(); + BassStep bassStep = factory.createBassStep(); + bassStep.setValue(stepOf(info.getBass().step)); + bass.setBassStep(bassStep); + + if (info.getBass().alter != 0) { + BassAlter bassAlter = factory.createBassAlter(); + bassAlter.setValue(new BigDecimal(info.getBass().alter)); + bass.setBassAlter(bassAlter); + } + harmony.getHarmonyChord().add(bass); + } + + // Degrees? + for (omr.score.entity.ChordInfo.Degree deg : info.getDegrees()) { + Degree degree = factory.createDegree(); + + DegreeValue value = factory.createDegreeValue(); + value.setValue(new BigInteger("" + deg.value)); + degree.setDegreeValue(value); + + DegreeAlter alter = factory.createDegreeAlter(); + alter.setValue(new BigDecimal(deg.alter)); + degree.setDegreeAlter(alter); + + DegreeType type = factory.createDegreeType(); + type.setValue(typeOf(deg.type)); + degree.setDegreeType(type); + + harmony.getHarmonyChord().add(degree); + } + + // Everything is now OK + current.pmMeasure.getNoteOrBackupOrForward().add(harmony); + } catch (Exception ex) { + logger.warn("Error visiting " + symbol, ex); + } + + return true; + } + + //----------------// + // visit Dynamics // + //----------------// + @Override + public boolean visit (Dynamics dynamics) + { + try { + logger.debug("Visiting {}", dynamics); + + // No point to export incorrect dynamics + if (dynamics.getShape() == null) { + return false; + } + + Direction direction = factory.createDirection(); + DirectionType directionType = factory.createDirectionType(); + com.audiveris.proxymusic.Dynamics pmDynamics = factory.createDynamics(); + + // Precise dynamic signature + pmDynamics.getPOrPpOrPpp().add( + getDynamicsObject(dynamics.getShape())); + + // Staff ? + Staff staff = current.note.getStaff(); + insertStaffId(direction, staff); + + // Placement + if (dynamics.getReferencePoint().y < current.note.getCenter().y) { + direction.setPlacement(AboveBelow.ABOVE); + } else { + direction.setPlacement(AboveBelow.BELOW); + } + + // default-y + pmDynamics.setDefaultY(yOf(dynamics.getReferencePoint(), staff)); + + // Relative-x (No offset for the time being) using note left side + pmDynamics.setRelativeX( + toTenths( + dynamics.getReferencePoint().x + - current.note.getCenterLeft().x)); + + // Related sound level, if available + Integer soundLevel = dynamics.getSoundLevel(); + + if (soundLevel != null) { + Sound sound = factory.createSound(); + sound.setDynamics(new BigDecimal(soundLevel)); + direction.setSound(sound); + } + + // Everything is now OK + directionType.getDynamics().add(pmDynamics); + direction.getDirectionType().add(directionType); + current.pmMeasure.getNoteOrBackupOrForward().add(direction); + } catch (Exception ex) { + logger.warn("Error visiting " + dynamics, ex); + } + + return false; + } + + //---------------// + // visit Fermata // + //---------------// + @Override + public boolean visit (Fermata fermata) + { + try { + logger.debug("Visiting {}", fermata); + + com.audiveris.proxymusic.Fermata pmFermata = factory.createFermata(); + + // default-y (of the fermata dot) + // For upright we use bottom of the box, for inverted the top of the box + Rectangle box = fermata.getBox(); + Point dot; + + if (fermata.getShape() == Shape.FERMATA_BELOW) { + dot = new Point(box.x + (box.width / 2), box.y); + } else { + dot = new Point( + box.x + (box.width / 2), + box.y + box.height); + } + + pmFermata.setDefaultY(yOf(dot, current.note.getStaff())); + + // Type + pmFermata. + setType( + (fermata.getShape() == Shape.FERMATA) ? UprightInverted.UPRIGHT + : UprightInverted.INVERTED); + // Everything is now OK + getNotations().getTiedOrSlurOrTuplet().add(pmFermata); + } catch (Exception ex) { + logger.warn("Error visiting " + fermata, ex); + } + + return false; + } + + //--------------------// + // visit KeySignature // + //--------------------// + @Override + public boolean visit (KeySignature keySignature) + { + try { + logger.debug("Visiting {}", keySignature); + + if (isNewKeySignature(keySignature)) { + Key key = factory.createKey(); + key.setFifths(new BigInteger("" + keySignature.getKey())); + + // Trick: add this key signature only if it does not already exist + List keys = getAttributes().getKey(); + + for (Key k : keys) { + if (areEqual(k, key)) { + return true; // Already inserted, so give up + } + } + + keys.add(key); + } + } catch (Exception ex) { + logger.warn("Error visiting " + keySignature, ex); + } + + return true; + } + + //---------------// + // visit Measure // + //---------------// + @Override + public boolean visit (Measure measure) + { + try { + logger.debug("Visiting {}", measure); + + // Make sure this measure is within the range to be exported + if (!isDesired(measure)) { + logger.debug("{} skipped.", measure); + return false; + } + + ///logger.info("Visiting " + measure); + logger.debug("{} : {}", measure, isFirst); + + current.measure = measure; + tupletNumbers.clear(); + + // Allocate Measure + current.pmMeasure = factory.createScorePartwisePartMeasure(); + current.pmMeasure.setNumber(measure.getScoreId()); + + if (measure.getWidth() != null) { + current.pmMeasure.setWidth(toTenths(measure.getWidth())); + } + + if (measure.isImplicit()) { + current.pmMeasure.setImplicit(YesNo.YES); + } + + // Do we need to create & export a dummy initial measure? + if (((measureRange != null) && !measure.isTemporary() + && (measure.getIdValue() > 1)) + && // TODO: Following line is illegal + (measure.getScoreId().equals(measureRange.getFirstId()))) { + insertCurrentContext(measure); + } + + // Print? + new MeasurePrint(measure).process(); + + // Inside barline? + visit(measure.getInsideBarline()); + + // Right Barline + if (!measure.isDummy()) { + visit(measure.getBarline()); + } + + // Left barline ? + Measure prevMeasure = (Measure) measure.getPreviousSibling(); + if ((prevMeasure != null) && !prevMeasure.isDummy()) { + visit(prevMeasure.getBarline()); + } + + // Divisions? + if (isFirst.page && isFirst.system && isFirst.measure) { + try { + getAttributes().setDivisions( + new BigDecimal( + score.simpleDurationOf( + omr.score.entity.Note.QUARTER_DURATION))); + } catch (Exception ex) { + if (score.getDurationDivisor() == null) { + logger.warn( + "Not able to infer division value for part {}", + current.scorePart.getPid()); + } else { + logger.warn("Error on divisions", ex); + } + } + } + + // Number of staves, if > 1 + if (isFirst.page && isFirst.system && isFirst.measure + && current.scorePart.isMultiStaff()) { + getAttributes().setStaves( + new BigInteger("" + current.scorePart.getStaffCount())); + + } + + // Tempo? + if (isFirst.page && isFirst.system && isFirst.measure + && !measure.isDummy()) { + Direction direction = factory.createDirection(); + current.pmMeasure.getNoteOrBackupOrForward().add(direction); + + DirectionType directionType = factory.createDirectionType(); + direction.getDirectionType().add(directionType); + + // Use a dummy words element + FormattedText pmWords = factory. + createFormattedText(); + directionType.getWords().add(pmWords); + pmWords.setValue(""); + + Sound sound = factory.createSound(); + sound.setTempo( + new BigDecimal(score.getTempoParam().getTarget())); + direction.setSound(sound); + } + + // Specific browsing down the measure + // Insert KeySignatures, TimeSignatures + measure.getKeySigList().acceptChildren(this); + measure.getTimeSigList().acceptChildren(this); + + // Clefs may be inserted further down the measure + ClefIterators clefIters = new ClefIterators(measure); + + // Insert clefs that occur before first time slot + List slots = measure.getSlots(); + + if (slots.isEmpty()) { + clefIters.push(null, null); + } else { + clefIters.push(slots.get(0).getX(), null); + } + + // Now voice per voice + Rational timeCounter = Rational.ZERO; + + for (Voice voice : measure.getVoices()) { + current.voice = voice; + + // Need a backup ? + if (!timeCounter.equals(Rational.ZERO)) { + insertBackup(timeCounter); + timeCounter = Rational.ZERO; + } + + if (voice.isWhole()) { + // Delegate to the chord children directly + Chord chord = voice.getWholeChord(); + clefIters.push(measure.getRightX(), chord.getStaff()); + chord.acceptChildren(this); + timeCounter = measure.getExpectedDuration(); + } else { + for (Slot slot : measure.getSlots()) { + VoiceChord info = voice.getSlotInfo(slot); + + if ((info != null) && // Skip free slots + (info.getStatus() == Voice.Status.BEGIN)) { + Chord chord = info.getChord(); + clefIters.push( + chord.getCenter().x, + chord.getStaff()); + + // Need a forward before this chord ? + Rational startTime = chord.getStartTime(); + + if (timeCounter.compareTo(startTime) < 0) { + insertForward( + startTime.minus(timeCounter), + chord); + timeCounter = startTime; + } + + // Delegate to the chord children directly + chord.acceptChildren(this); + timeCounter = timeCounter.plus(chord.getDuration()); + } + } + + // Need an ending forward ? + if (!measure.isImplicit() && !measure.isFirstHalf()) { + Rational termination = voice.getTermination(); + + if ((termination != null) + && (termination.compareTo(Rational.ZERO) < 0)) { + Rational delta = termination.opposite(); + insertForward(delta, voice.getLastChord()); + timeCounter = timeCounter.plus(delta); + } + } + } + } + + // Clefs that occur after time slots, if any + clefIters.push(null, null); + + // Everything is now OK + current.pmPart.getMeasure().add(current.pmMeasure); + } catch (Exception ex) { + logger.warn("Error visiting " + measure + " in " + current.page, ex); + } + + // Safer... + current.endMeasure(); + tupletNumbers.clear(); + isFirst.measure = false; + + return false; // Not this way + } + + //------------// + // visit Note // + //------------// + @Override + public boolean visit (omr.score.entity.Note note) + { + try { + logger.debug("Visiting {}", note); + + current.note = note; + + Chord chord = note.getChord(); + + // For first note in chord + if (chord.getNotes().indexOf(note) == 0) { + // Chord direction events + for (omr.score.entity.Direction node : chord.getDirections()) { + node.accept(this); + } + // Chord symbol, if any + if (chord.getChordSymbol() != null) { + chord.getChordSymbol().accept(this); + } + } + + current.pmNote = factory.createNote(); + + Staff staff = note.getStaff(); + + // Chord notation events for first note in chord + if (chord.getNotes().indexOf(note) == 0) { + for (Notation node : chord.getNotations()) { + node.accept(this); + } + } else { + // Chord indication for every other note + current.pmNote.setChord(new Empty()); + + // Arpeggiate also? + for (Notation node : chord.getNotations()) { + if (node instanceof Arpeggiate) { + node.accept(this); + } + } + } + + // Rest ? + if (note.isRest()) { + Rest rest = factory.createRest(); + + // Rest for the whole measure? + if (chord.isWholeDuration()) { + rest.setMeasure(YesNo.YES); + } + + /// TODO ??? Set Step or Octave ??? + current.pmNote.setRest(rest); + } else { + // Pitch + Pitch pitch = factory.createPitch(); + pitch.setStep(stepOf(note.getStep())); + pitch.setOctave(note.getOctave()); + + if (note.getAlter() != 0) { + pitch.setAlter(new BigDecimal(note.getAlter())); + } + + current.pmNote.setPitch(pitch); + } + + // Default-x (use left side of the note wrt measure) + if (!note.getMeasure().isDummy()) { + int noteLeft = note.getCenterLeft().x; + current.pmNote.setDefaultX( + toTenths(noteLeft - note.getMeasure().getLeftX())); + } + + // Tuplet factor ? + if (chord.getTupletFactor() != null) { + TimeModification timeModification = factory. + createTimeModification(); + timeModification.setActualNotes( + new BigInteger("" + chord.getTupletFactor().actualDen)); + timeModification.setNormalNotes( + new BigInteger("" + chord.getTupletFactor().actualNum)); + current.pmNote.setTimeModification(timeModification); + } + + // Duration + try { + Rational dur; + + if (chord.isWholeDuration()) { + dur = chord.getMeasure().getActualDuration(); + } else { + dur = chord.getDuration(); + } + + current.pmNote.setDuration( + new BigDecimal(score.simpleDurationOf(dur))); + } catch (Exception ex) { + if (score.getDurationDivisor() != null) { + logger.warn("Not able to get duration of note", ex); + } + } + + // Voice + current.pmNote.setVoice("" + chord.getVoice().getId()); + + // Type + if (!note.getMeasure().isDummy()) { + NoteType noteType = factory.createNoteType(); + noteType.setValue("" + getNoteTypeName(note)); + + if (!chord.isWholeDuration()) { + current.pmNote.setType(noteType); + } + } + + // For specific mirrored note + if (note.getMirroredNote() != null) { + int fbn = note.getChord().getFlagsNumber() + + note.getChord().getBeams().size(); + + if ((fbn > 0) && (note.getShape() == NOTEHEAD_VOID)) { + // Indicate that the head should not be filled + // normal + Notehead notehead = factory.createNotehead(); + notehead.setFilled(YesNo.NO); + notehead.setValue(NoteheadValue.NORMAL); + current.pmNote.setNotehead(notehead); + } + } + + // Stem ? + if (chord.getStem() != null) { + Stem pmStem = factory.createStem(); + Point tail = chord.getTailLocation(); + pmStem.setDefaultY(yOf(tail, staff)); + + if (tail.y < note.getCenter().y) { + pmStem.setValue(StemValue.UP); + } else { + pmStem.setValue(StemValue.DOWN); + } + + current.pmNote.setStem(pmStem); + } + + // Staff ? + if (current.scorePart.isMultiStaff()) { + current.pmNote.setStaff(new BigInteger("" + staff.getId())); + } + + // Dots + for (int i = 0; i < chord.getDotsNumber(); i++) { + current.pmNote.getDot().add(factory.createEmptyPlacement()); + } + + // Accidental ? + if (note.getAccidental() != null) { + Accidental accidental = factory.createAccidental(); + accidental.setValue( + accidentalValueOf(note.getAccidental().getShape())); + current.pmNote.setAccidental(accidental); + } + + // Beams ? + for (Beam beam : chord.getBeams()) { + com.audiveris.proxymusic.Beam pmBeam = factory.createBeam(); + pmBeam.setNumber(1 + chord.getBeams().indexOf(beam)); + + if (beam.isHook()) { + if (beam.getCenter().x > chord.getStem().getLocation().x) { + pmBeam.setValue(BeamValue.FORWARD_HOOK); + } else { + pmBeam.setValue(BeamValue.BACKWARD_HOOK); + } + } else { + List chords = beam.getChords(); + if (chords.get(0) == chord) { + pmBeam.setValue(BeamValue.BEGIN); + } else if (chords.get(chords.size() - 1) == chord) { + pmBeam.setValue(BeamValue.END); + } else { + pmBeam.setValue(BeamValue.CONTINUE); + } + } + + current.pmNote.getBeam().add(pmBeam); + } + + // Ties / Slurs + for (Slur slur : note.getSlurs()) { + slur.accept(this); + } + + // Lyrics ? + if (note.getSyllables() != null) { + for (LyricsItem syllable : note.getSyllables()) { + if (syllable.getContent() != null) { + Lyric pmLyric = factory.createLyric(); + pmLyric.setDefaultY( + yOf(syllable.getReferencePoint(), staff)); + pmLyric.setNumber( + "" + syllable.getLyricsLine().getId()); + + TextElementData pmText = factory.createTextElementData(); + pmText.setValue(syllable.getContent()); + pmLyric.getElisionAndSyllabicAndText(). + add(getSyllabic(syllable. + getSyllabicType())); + pmLyric.getElisionAndSyllabicAndText().add(pmText); + + current.pmNote.getLyric().add(pmLyric); + } + } + } + + // Everything is OK + current.pmMeasure.getNoteOrBackupOrForward().add(current.pmNote); + } catch (Exception ex) { + logger.warn("Error visiting " + note, ex); + } + + // Safer... + current.endNote(); + + return true; + } + + //----------------// + // visit Ornament // + //----------------// + @Override + @SuppressWarnings("unchecked") + public boolean visit (Ornament ornament) + { + try { + logger.debug("Visiting {}", ornament); + + JAXBElement element = getOrnamentObject(ornament.getShape()); + + // Placement? + Class classe = element.getDeclaredType(); + Method method = classe.getMethod( + "setPlacement", + AboveBelow.class); + method.invoke( + element.getValue(), + (ornament.getReferencePoint().y < current.note.getCenter().y) + ? AboveBelow.ABOVE : AboveBelow.BELOW); + // Everything is OK + // Include in ornaments + getOrnaments().getTrillMarkOrTurnOrDelayedTurn().add(element); + } catch (Exception ex) { + logger.warn("Error visiting " + ornament, ex); + } + + return false; + } + + //------------// + // visit Page // + //------------// + @Override + public boolean visit (Page page) + { + try { + logger.debug("Visiting {}", page); + + isFirst.page = (page == score.getFirstPage()); + isFirst.system = true; + isFirst.measure = true; + current.page = page; + + Page prevPage = (Page) page.getPreviousSibling(); + current.pageMeasureIdOffset = (prevPage == null) ? 0 + : (current.pageMeasureIdOffset + + prevPage.getDeltaMeasureId()); + current.scale = page.getScale(); + } catch (Exception ex) { + logger.warn("Error visiting " + page, ex); + } + + return true; + } + + //-------------// + // visit Pedal // + //-------------// + @Override + public boolean visit (Pedal pedal) + { + try { + logger.debug("Visiting {}", pedal); + + Direction direction = new Direction(); + DirectionType directionType = new DirectionType(); + com.audiveris.proxymusic.Pedal pmPedal = new com.audiveris.proxymusic.Pedal(); + + // No line (for the time being) + pmPedal.setLine(YesNo.NO); + + // Start / Stop type + pmPedal.setType( + pedal.isStart() + ? StartStopChangeContinue.START + : StartStopChangeContinue.STOP); + + // Staff ? + Staff staff = current.note.getStaff(); + insertStaffId(direction, staff); + + // default-x + pmPedal.setDefaultX( + toTenths( + pedal.getReferencePoint().x - current.measure.getLeftX())); + + // default-y + pmPedal.setDefaultY(yOf(pedal.getReferencePoint(), staff)); + + // Placement + direction.setPlacement( + (pedal.getReferencePoint().y < current.note.getCenter().y) + ? AboveBelow.ABOVE : AboveBelow.BELOW); + // Everything is OK + directionType.setPedal(pmPedal); + direction.getDirectionType().add(directionType); + current.pmMeasure.getNoteOrBackupOrForward().add(direction); + } catch (Exception ex) { + logger.warn("Error visiting " + pedal, ex); + } + + return true; + } + + //-------------// + // visit Score // + //-------------// + /** + * Allocate/populate everything that directly relates to the score + * instance. + * The rest of processing is delegated to the score children, that is to + * say pages (TBI), then systems, etc... + * + * @param score visit the score to export + * @return false, since no further processing is required after this node + */ + @Override + public boolean visit (Score score) + { + try { + logger.debug("Visiting {}", score); + + // Reset durations for the score + score.setDurationDivisor(null); + + // No version inserted + // Let the marshalling class handle it + + // Identification + Identification identification = factory.createIdentification(); + + // Source + identification.setSource(score.getImagePath()); + + // Encoding + Encoding encoding = factory.createEncoding(); + scorePartwise.setIdentification(identification); + + // [Encoding]/Software + encoding.getEncodingDateOrEncoderOrSoftware().add( + factory.createEncodingSoftware( + WellKnowns.TOOL_NAME + " " + WellKnowns.TOOL_REF)); + + // [Encoding]/EncodingDate + // Let the Marshalling class handle it + identification.setEncoding(encoding); + + // Defaults + Defaults defaults = new Defaults(); + + // [Defaults]/Scaling (using first page) + Page firstPage = score.getFirstPage(); + + if (current.scale == null) { + current.scale = firstPage.getScale(); + } + + if (current.scale != null) { + Scaling scaling = factory.createScaling(); + defaults.setScaling(scaling); + scaling.setMillimeters( + new BigDecimal( + String.format("%.4f", (current.scale.getInterline() * 25.4 * 4) / 300))); // Assuming 300 DPI + scaling.setTenths(new BigDecimal(40)); + + // [Defaults]/PageLayout (using first page) + if (firstPage.getDimension() != null) { + PageLayout pageLayout = factory.createPageLayout(); + defaults.setPageLayout(pageLayout); + pageLayout.setPageHeight( + toTenths(firstPage.getDimension().height)); + pageLayout.setPageWidth( + toTenths(firstPage.getDimension().width)); + + PageMargins pageMargins = factory.createPageMargins(); + pageMargins.setType(MarginType.BOTH); + pageMargins.setLeftMargin(pageHorizontalMargin); + pageMargins.setRightMargin(pageHorizontalMargin); + pageMargins.setTopMargin(pageVerticalMargin); + pageMargins.setBottomMargin(pageVerticalMargin); + pageLayout.getPageMargins().add(pageMargins); + } + } + + // [Defaults]/LyricFont + Font lyricFont = omr.score.entity.Text.getLyricsFont(); + LyricFont pmLyricFont = factory.createLyricFont(); + pmLyricFont.setFontFamily(lyricFont.getName()); + pmLyricFont.setFontSize( + "" + omr.score.entity.Text.getLyricsFontSize()); + if (lyricFont.isItalic()) { + pmLyricFont.setFontStyle(FontStyle.ITALIC); + } + defaults.getLyricFont().add(pmLyricFont); + scorePartwise.setDefaults(defaults); + + // PartList & sequence of parts + if (score.getPartList() != null) { + PartList partList = factory.createPartList(); + scorePartwise.setPartList(partList); + + // Here we browse the score hierarchy once for each score scorePart + isFirst.scorePart = true; + + for (ScorePart p : score.getPartList()) { + partList.getPartGroupOrScorePart().add(getScorePart(p)); + isFirst.scorePart = false; + } + } + } catch (Exception ex) { + logger.warn("Error visiting " + score, ex); + } + + return false; // We don't go this way + } + + //-------------------// + // visit ScoreSystem // + //-------------------// + /** + * Allocate/populate everything that directly relates to this + * system in the current scorePart. + * The rest of processing is directly delegated to the measures + * + * @param system the system to export + * @return false + */ + @Override + public boolean visit (ScoreSystem system) + { + try { + logger.debug("Visiting {}", system); + + current.system = system; + isFirst.measure = true; + + SystemPart systemPart = system.getPart(current.scorePart.getId()); + + if (systemPart != null) { + systemPart.accept(this); + } else { + // Need to build an artificial system scorePart + // Or simply delegating to the series of artificial measures + SystemPart dummyPart = system.getFirstRealPart(). + createDummyPart( + current.scorePart.getId()); + dummyPart.accept(this); + } + + // If we have exported a measure, we are no longer in the first system + if (!isFirst.measure) { + isFirst.system = false; + } + } catch (Exception ex) { + logger.warn("Error visiting " + system, ex); + } + + return false; // No default browsing this way + } + + //-------------// + // visit Segno // + //-------------// + @Override + public boolean visit (Segno segno) + { + try { + logger.debug("Visiting {}", segno); + + Direction direction = new Direction(); + DirectionType directionType = factory.createDirectionType(); + + EmptyPrintStyleAlign empty = factory.createEmptyPrintStyleAlign(); + + // Staff ? + Staff staff = current.note.getStaff(); + insertStaffId(direction, staff); + + // default-x + empty.setDefaultX( + toTenths( + segno.getReferencePoint().x - current.measure.getLeftX())); + + // default-y + empty.setDefaultY(yOf(segno.getReferencePoint(), staff)); + + // Need also a Sound element (TODO: We don't do anything with sound!) + Sound sound = factory.createSound(); + sound.setSegno("" + current.measure.getScoreId()); + sound.setDivisions( + new BigDecimal( + score.simpleDurationOf( + omr.score.entity.Note.QUARTER_DURATION))); + + // Everything is OK + directionType.getSegno().add(empty); + direction.getDirectionType().add(directionType); + current.pmMeasure.getNoteOrBackupOrForward().add(direction); + } catch (Exception ex) { + logger.warn("Error visiting " + segno, ex); + } + + return true; + } + + //------------// + // visit Slur // + //------------// + @Override + public boolean visit (Slur slur) + { + try { + logger.debug("Visiting {}", slur); + + // Make sure we have notes (or extension) on both sides + // TODO: Make an exception for slurs at beginning of page! + if ((slur.getLeftNote() == null) + && (slur.getLeftExtension() == null)) { + slur.addError("Non left-connected slur is not exported"); + + return false; + } + + // TODO: Make an exception for slurs at end of page! + if ((slur.getRightNote() == null) + && (slur.getRightExtension() == null)) { + slur.addError("Non right-connected slur is not exported"); + + return false; + } + + // Note contextual data + boolean isStart = slur.getLeftNote() == current.note; + int noteLeft = current.note.getCenterLeft().x; + Staff staff = current.note.getStaff(); + + if (slur.isTie()) { + // Tie element + Tie tie = factory.createTie(); + tie.setType(isStart ? StartStop.START : StartStop.STOP); + current.pmNote.getTie().add(tie); + + // Tied element + Tied tied = factory.createTied(); + + // Type + tied.setType(isStart ? StartStopContinue.START : StartStopContinue.STOP); + + // Orientation + if (isStart) { + tied.setOrientation( + slur.isBelow() ? OverUnder.UNDER : OverUnder.OVER); + } + + // Bezier + if (isStart) { + tied.setDefaultX( + toTenths(slur.getCurve().getX1() - noteLeft)); + tied.setDefaultY(yOf(slur.getCurve().getY1(), staff)); + tied.setBezierX( + toTenths(slur.getCurve().getCtrlX1() - noteLeft)); + tied.setBezierY(yOf(slur.getCurve().getCtrlY1(), staff)); + } else { + tied.setDefaultX( + toTenths(slur.getCurve().getX2() - noteLeft)); + tied.setDefaultY(yOf(slur.getCurve().getY2(), staff)); + tied.setBezierX( + toTenths(slur.getCurve().getCtrlX2() - noteLeft)); + tied.setBezierY(yOf(slur.getCurve().getCtrlY2(), staff)); + } + + getNotations().getTiedOrSlurOrTuplet().add(tied); + } else { + // Slur element + com.audiveris.proxymusic.Slur pmSlur = factory.createSlur(); + + // Number attribute + Integer num = slurNumbers.get(slur); + + if (num != null) { + pmSlur.setNumber(num); + slurNumbers.remove(slur); + + logger.debug("{} last use {} -> {}", + current.note.getContextString(), + num, slurNumbers.toString()); + } else { + // Determine first available number + for (num = 1; num <= 6; num++) { + if (!slurNumbers.containsValue(num)) { + if (slur.getRightExtension() != null) { + slurNumbers.put(slur.getRightExtension(), num); + } else { + slurNumbers.put(slur, num); + } + + pmSlur.setNumber(num); + + logger.debug("{} first use {} -> {}", + current.note.getContextString(), + num, slurNumbers.toString()); + + break; + } + } + } + + // Type + pmSlur. + setType( + isStart ? StartStopContinue.START : StartStopContinue.STOP); + + // Placement + if (isStart) { + pmSlur. + setPlacement( + slur.isBelow() ? AboveBelow.BELOW : AboveBelow.ABOVE); + } + + // Bezier + if (isStart) { + pmSlur.setDefaultX( + toTenths(slur.getCurve().getX1() - noteLeft)); + pmSlur.setDefaultY(yOf(slur.getCurve().getY1(), staff)); + pmSlur.setBezierX( + toTenths(slur.getCurve().getCtrlX1() - noteLeft)); + pmSlur.setBezierY(yOf(slur.getCurve().getCtrlY1(), staff)); + } else { + pmSlur.setDefaultX( + toTenths(slur.getCurve().getX2() - noteLeft)); + pmSlur.setDefaultY(yOf(slur.getCurve().getY2(), staff)); + pmSlur.setBezierX( + toTenths(slur.getCurve().getCtrlX2() - noteLeft)); + pmSlur.setBezierY(yOf(slur.getCurve().getCtrlY2(), staff)); + } + + getNotations().getTiedOrSlurOrTuplet().add(pmSlur); + } + } catch (Exception ex) { + logger.warn("Error visiting " + slur, ex); + } + + return true; + } + + //------------------// + // visit SystemPart // + //------------------// + @Override + public boolean visit (SystemPart systemPart) + { + try { + logger.debug("Visiting {}", systemPart); + + // Delegate to texts + for (TreeNode node : systemPart.getTexts()) { + ((Text) node).accept(this); + } + + // Delegate to measures + for (TreeNode node : systemPart.getMeasures()) { + ((Measure) node).accept(this); + } + } catch (Exception ex) { + logger.warn("Error visiting " + systemPart, ex); + } + + return false; // No default browsing this way + } + + //------------// + // visit Text // + //------------// + @Override + public boolean visit (Text text) + { + try { + logger.debug("Visiting {}", text); + + switch (text.getSentence().getRole().role) { + case Title: + getWork().setWorkTitle(text.getContent()); + + break; + + case Number: + getWork().setWorkNumber(text.getContent()); + + break; + + case Rights: { + TypedText typedText = factory.createTypedText(); + typedText.setValue(text.getContent()); + scorePartwise.getIdentification().getRights().add(typedText); + } + + break; + + case Creator: { + TypedText typedText = factory.createTypedText(); + typedText.setValue(text.getContent()); + + CreatorType type = text.getSentence().getRole().creatorType; + + if (type != null) { + typedText.setType(type.toString()); + } + + scorePartwise.getIdentification().getCreator().add(typedText); + } + + break; + + case UnknownRole: + break; + + default: // LyricsItem, Direction, Chord + + // Handle them through related Note + return false; + } + + // Credits + Credit pmCredit = factory.createCredit(); + // For MusicXML, page # is counted from 1, whatever the pageIndex + pmCredit.setPage( + new BigInteger("" + (1 + current.page.getChildIndex()))); + + FormattedText creditWords = factory.createFormattedText(); + creditWords.setValue(text.getContent()); + + // Font information + setFontInfo(creditWords, text); + + // Position is wrt page + Point pt = text.getReferencePoint(); + creditWords.setDefaultX(toTenths(pt.x)); + creditWords.setDefaultY( + toTenths(current.page.getDimension().height - pt.y)); + + pmCredit.getCreditTypeOrLinkOrBookmark().add(creditWords); + scorePartwise.getCredit().add(pmCredit); + } catch (Exception ex) { + logger.warn("Error visiting " + text, ex); + } + + return true; + } + + //---------------------// + // visit TimeSignature // + //---------------------// + @Override + public boolean visit (TimeSignature timeSignature) + { + try { + logger.debug("Visiting {}", timeSignature); + + try { + Time time = factory.createTime(); + + // Beats + time.getTimeSignature().add( + factory.createTimeBeats( + "" + timeSignature.getNumerator())); + + // BeatType + time.getTimeSignature().add( + factory.createTimeBeatType( + "" + timeSignature.getDenominator())); + + // Symbol ? + if (timeSignature.getShape() != null) { + switch (timeSignature.getShape()) { + case COMMON_TIME: + time.setSymbol(TimeSymbol.COMMON); + + break; + + case CUT_TIME: + time.setSymbol(TimeSymbol.CUT); + + break; + } + } + + // Trick: add this time signature only if it does not already exist + List