1# -*- coding: utf-8 -*-
2# ------------------------------------------------------------------------------
3# Name:         layout.py
4# Purpose:      Layout objects
5#
6# Authors:      Christopher Ariza
7#               Michael Scott Cuthbert
8#
9# Copyright:    Copyright © 2010, 2012 Michael Scott Cuthbert and the music21 Project
10# License:      BSD, see license.txt
11# ------------------------------------------------------------------------------
12'''
13The layout.py module contains two types of objects that specify the layout on
14page (or screen) for Scores and other Stream objects.  There are two main types
15of Layout objects: (1) layout describing elements and (2) layout defining Streams.
16
17(1) ScoreLayout, PageLayout, SystemLayout, and StaffLayout objects describe the size of
18pages, the geometry of page and system margins, the distance between staves, etc.
19The model for these layout objects is taken directly (perhaps too directly?)
20from MusicXML.  These objects all inherit from a BaseLayout class, primarily
21as an aid to finding all of these objects as a group.  ScoreLayouts give defaults
22for each page, system, and staff.  Thus they contain PageLayout, SystemLayout, and
23currently one or more StaffLayout objects (but probably just one. MusicXML allows more than
24StaffLayout object because multiple staves can be in a Part.  Music21 uses
25the concept of a PartStaff for a Part that is played by the same performer as another.
26e.g., the left hand of the Piano is a PartStaff paired with the right hand).
27
28PageLayout and SystemLayout objects also have a property, 'isNew',
29which if set to `True` signifies that a new page
30or system should begin here.  In theory, one could define new dimensions for a page
31or system in the middle of the system or page without setting isNew to True, in
32which case these measurements would start applying on the next page.  In practice,
33there's really one good place to use these Layout objects and that's in the first part
34in a score at offset 0 of the first measure on a page or system
35(or for ScoreLayout, at the beginning
36of a piece outside of any parts).  But it's not an
37error to put them in other places, such as at offset 0 of the first measure of a page
38or system in all the other parts.  In fact, MusicXML tends to do this, and it ends up
39not being a waste if a program extracts a single part from the middle of a score.
40
41These objects are standard :class:`~music21.base.Music21Object` types, but many
42properties such as .duration, .beat, will probably not apply.
43
44When exporting to MusicXML (which is currently the only format in which music21 can and
45does preserve these markings), many MusicXML readers will ignore these tags (or worse,
46add a new page or system when PageLayout and SystemLayout objects are found but also
47add theme wherever they want).  In Finale, this behavior disappears if the MusicXML
48document notes that it <supports> new-page and new-system markings.  Music21 will add
49the appropriate <supports> tags if the containing Stream has `.definesExplicitPageBreaks`
50and `.definesExplicitSystemBreaks` set to True.  When importing a score that has the
51<supports> tag set, music21 will set `.definesExplicitXXXXBreaks` to True for the
52outer score and the inner parts.  However, this means that if the score is manipulated
53enough that the prior layout information is obsolete, programs will need to set these
54properties to False or move the Layout tags accordingly.
55
56(2) The second set of objects are Stream subclasses that can be employed when a program
57needs to easily iterate around the systems and pages defined through the layout objects
58just described, or to get the exact position on a page (or a graphical representation
59of a page) for a particular measure or system.  (Individual notes coming soon).  Normal
60Score streams can be changed into LayoutStreams by calling `divideByPages(s)` on them.
61A Score that was organized: Score->Parts->Measures would then become:
62LayoutScore->Pages->Systems->Parts->Measures.
63
64The new LayoutScore has methods that enable querying what page or system a measure is in, and
65specifically where on a page a measure is (or the dimensions
66of every measure in the piece).  However
67do not call .show() on a LayoutScore -- the normal score it's derived from will work just fine.
68Nor does calling .show() on a Page or System work yet, but once the LayoutStream has been created,
69code like this can be done:
70
71     s = stream.Stream(...)
72     ls = layout.divideByPages(s)
73     pg2sys3 = ls.pages[1].systems[2]  # n.b.! 1, 2
74     measureStart, measureEnd = pg2sys3.measureStart, pg2sys3.measureEnd
75     scoreExcerpt = s.measures(measureStart, measureEnd)
76     scoreExcerpt.show()  # will show page 2, system 3
77
78Note that while the coordinates given by music21 for a musicxml score (based on margins,
79staff size, etc.)
80generally reflect what is actually in a musicxml producer, unfortunately, x-positions are
81far less accurately
82produced by most editors.  For instance, Finale scores with measure sizes that have been
83manually adjusted tend to show their
84unadjusted measure width and not their actual measure width in the MusicXML.
85
86SmartScore Pro tends to produce very good MusicXML layout data.
87'''
88
89# may need to have object to convert between size units
90import copy
91import unittest
92
93from collections import namedtuple
94
95from music21 import base
96from music21 import exceptions21
97from music21 import spanner
98from music21 import stream
99from music21.stream.enums import StaffType
100
101from music21 import environment
102_MOD = 'layout'
103environLocal = environment.Environment(_MOD)
104
105
106SystemSize = namedtuple('SystemSize', 'top left right bottom')
107PageSize = namedtuple('PageSize', 'top left right bottom width height')
108
109
110class LayoutBase(base.Music21Object):
111    '''
112    A base class for all Layout objects, defining a classSortOrder
113    and also an inheritance tree.
114
115    >>> scoreLayout = layout.ScoreLayout()
116    >>> isinstance(scoreLayout, layout.LayoutBase)
117    True
118    '''
119    classSortOrder = -10
120
121    def __init__(self, *args, **keywords):
122        super().__init__()
123
124    def _reprInternal(self):
125        return ''
126
127# ------------------------------------------------------------------------------
128
129
130class ScoreLayout(LayoutBase):
131    '''Parameters for configuring a score's layout.
132
133    PageLayout objects may be found on Measure or Part Streams.
134
135
136    >>> pl = layout.PageLayout(pageNumber=4, leftMargin=234, rightMargin=124,
137    ...                        pageHeight=4000, pageWidth=3000, isNew=True)
138    >>> pl.pageNumber
139    4
140    >>> pl.rightMargin
141    124
142    >>> pl.leftMargin
143    234
144    >>> pl.isNew
145    True
146
147    This object represents both <print new-page> and <page-layout>
148    elements in musicxml
149
150    TODO -- make sure that the first pageLayout and systemLayout
151    for each page are working together.
152    '''
153
154    def __init__(self, *args, **keywords):
155        super().__init__()
156
157        self.scalingMillimeters = None
158        self.scalingTenths = None
159        self.pageLayout = None
160        self.systemLayout = None
161        self.staffLayoutList = []
162        self.appearance = None
163        self.musicFont = None
164        self.wordFont = None
165
166        for key in keywords:
167            if key.lower() == 'scalingmillimeters':
168                self.scalingMillimeters = keywords[key]
169            elif key.lower() == 'scalingtenths':
170                self.scalingTenths = keywords[key]
171            elif key.lower() == 'pagelayout':
172                self.rightMargin = keywords[key]
173            elif key.lower() == 'systemlayout':
174                self.systemLayout = keywords[key]
175            elif key.lower() == 'stafflayout':
176                self.staffLayoutList = keywords[key]
177            elif key.lower() == 'appearance':
178                self.appearance = keywords[key]
179            elif key.lower() == 'musicfont':
180                self.musicFont = keywords[key]
181            elif key.lower() == 'wordfont':
182                self.wordFont = keywords[key]
183
184    def tenthsToMillimeters(self, tenths):
185        '''
186        given the scalingMillimeters and scalingTenths,
187        return the value in millimeters of a number of
188        musicxml "tenths" where a tenth is a tenth of the distance
189        from one staff line to another
190
191        returns 0.0 if either of scalingMillimeters or scalingTenths
192        is undefined.
193
194
195        >>> sl = layout.ScoreLayout(scalingMillimeters=2.0, scalingTenths=10)
196        >>> print(sl.tenthsToMillimeters(10))
197        2.0
198        >>> print(sl.tenthsToMillimeters(17))  # printing to round
199        3.4
200        '''
201        if self.scalingMillimeters is None or self.scalingTenths is None:
202            return 0.0
203        millimetersPerTenth = float(self.scalingMillimeters) / self.scalingTenths
204        return round(millimetersPerTenth * tenths, 6)
205
206
207# ------------------------------------------------------------------------------
208class PageLayout(LayoutBase):
209    '''
210    Parameters for configuring a page's layout.
211
212    PageLayout objects may be found on Measure or Part Streams.
213
214
215    >>> pl = layout.PageLayout(pageNumber=4, leftMargin=234, rightMargin=124,
216    ...                        pageHeight=4000, pageWidth=3000, isNew=True)
217    >>> pl.pageNumber
218    4
219    >>> pl.rightMargin
220    124
221    >>> pl.leftMargin
222    234
223    >>> pl.isNew
224    True
225
226    This object represents both <print new-page> and <page-layout>
227    elements in musicxml
228
229    ## TODO -- make sure that the first pageLayout and systemLayout
230    for each page are working together.
231
232    '''
233
234    def __init__(self, *args, **keywords):
235        super().__init__()
236
237        self.pageNumber = None
238        self.leftMargin = None
239        self.rightMargin = None
240        self.topMargin = None
241        self.bottomMargin = None
242        self.pageHeight = None
243        self.pageWidth = None
244
245        # store if this is the start of a new page
246        self.isNew = None
247
248        for key in keywords:
249            if key.lower() == 'pagenumber':
250                self.pageNumber = keywords[key]
251            elif key.lower() == 'leftmargin':
252                self.leftMargin = keywords[key]
253            elif key.lower() == 'rightmargin':
254                self.rightMargin = keywords[key]
255            elif key.lower() == 'topmargin':
256                self.topMargin = keywords[key]
257            elif key.lower() == 'bottommargin':
258                self.bottomMargin = keywords[key]
259            elif key.lower() == 'pageheight':
260                self.pageHeight = keywords[key]
261            elif key.lower() == 'pagewidth':
262                self.pageWidth = keywords[key]
263            elif key.lower() == 'isnew':
264                self.isNew = keywords[key]
265
266# ------------------------------------------------------------------------------
267
268
269class SystemLayout(LayoutBase):
270    '''
271    Object that configures or alters a system's layout.
272
273    SystemLayout objects may be found on Measure or
274    Part Streams.
275
276    Importantly, if isNew is True then this object
277    indicates that a new system should start here.
278
279
280    >>> sl = layout.SystemLayout(leftMargin=234, rightMargin=124, distance=3, isNew=True)
281    >>> sl.distance
282    3
283    >>> sl.rightMargin
284    124
285    >>> sl.leftMargin
286    234
287    >>> sl.isNew
288    True
289    '''
290
291    def __init__(self, *args, **keywords):
292        super().__init__()
293
294        self.leftMargin = None
295        self.rightMargin = None
296        # no top or bottom margins
297
298        # this is probably the distance between adjacent systems
299        self.distance = None
300        self.topDistance = None
301
302        # store if this is the start of a new system
303        self.isNew = None
304
305        for key in keywords:
306            if key.lower() == 'leftmargin':
307                self.leftMargin = keywords[key]
308            elif key.lower() == 'rightmargin':
309                self.rightMargin = keywords[key]
310
311            elif key.lower() == 'distance':
312                self.distance = keywords[key]
313            elif key.lower() == 'topdistance':
314                self.topDistance = keywords[key]
315            elif key.lower() == 'isnew':
316                self.isNew = keywords[key]
317
318
319class StaffLayout(LayoutBase):
320    '''
321    Object that configures or alters the distance between
322    one staff and another in a system.
323
324    StaffLayout objects may be found on Measure or
325    Part Streams.
326
327    The musicxml equivalent <staff-layout> lives in
328    the <defaults> and in <print> attributes.
329
330
331    >>> sl = layout.StaffLayout(distance=3, staffNumber=1, staffSize=113, staffLines=5)
332    >>> sl.distance
333    3
334
335    The "number" attribute refers to which staff number
336    in a part group this refers to.  Thus, it's not
337    necessary in music21, but we store it if it's there.
338    (defaults to None)
339
340    >>> sl.staffNumber
341    1
342
343    staffLines specifies the number of lines for a non 5-line staff.
344
345    >>> sl.staffLines
346    5
347
348    staffSize is a percentage of the base staff size, so
349    this defines a staff 13% larger than normal.
350
351    >>> sl.staffSize
352    113.0
353    >>> sl
354    <music21.layout.StaffLayout distance 3, staffNumber 1, staffSize 113.0, staffLines 5>
355
356
357    StaffLayout can also specify the staffType:
358
359    >>> sl.staffType = stream.enums.StaffType.OSSIA
360
361    There is one other attribute, '.hidden' which has three settings:
362
363    * None - inherit from previous StaffLayout object, or False if no object exists
364    * False - not hidden -- show as a default staff
365    * True - hidden -- for playback only staves, or for a hidden/optimized-out staff
366
367    Note: (TODO: .hidden None is not working; always gives False)
368    '''
369    _DOC_ATTR = {
370        'staffType': '''
371            What kind of staff is this as a stream.enums.StaffType.
372
373            >>> sl = layout.StaffLayout()
374            >>> sl.staffType
375            <StaffType.REGULAR: 'regular'>
376            >>> sl.staffType = stream.enums.StaffType.CUE
377            >>> sl.staffType
378            <StaffType.CUE: 'cue'>
379            ''',
380    }
381    def __init__(self, *args, **keywords):
382        super().__init__()
383
384        # this is the distance between adjacent staves
385        self.distance = None
386        self.staffNumber = None
387        self.staffSize = None
388        self.staffLines = None
389        self.hidden = None  # True = hidden; False = shown; None = inherit
390        self.staffType: StaffType = StaffType.REGULAR
391
392        for key in keywords:
393            keyLower = key.lower()
394            if keyLower == 'distance':
395                self.distance = keywords[key]
396            elif keyLower == 'staffnumber':
397                self.staffNumber = keywords[key]
398            elif keyLower == 'staffsize':
399                if keywords[key] is not None:
400                    self.staffSize = float(keywords[key])
401            elif keyLower == 'stafflines':
402                self.staffLines = keywords[key]
403            elif keyLower == 'hidden':
404                if keywords[key] is not False and keywords[key] is not None:
405                    self.hidden = True
406            elif keyLower == 'staffType':
407                self.staffType = keywords[key]
408
409    def _reprInternal(self):
410        return (f'distance {self.distance!r}, staffNumber {self.staffNumber!r}, '
411                + f'staffSize {self.staffSize!r}, staffLines {self.staffLines!r}')
412
413# ------------------------------------------------------------------------------
414
415
416class LayoutException(exceptions21.Music21Exception):
417    pass
418
419
420class StaffGroupException(spanner.SpannerException):
421    pass
422
423
424# ------------------------------------------------------------------------------
425class StaffGroup(spanner.Spanner):
426    '''
427    A StaffGroup defines a collection of one or more
428    :class:`~music21.stream.Part` objects,
429    specifying that they should be shown together with a bracket,
430    brace, or other symbol, and may have a common name.
431
432    >>> p1 = stream.Part()
433    >>> p2 = stream.Part()
434    >>> p1.append(note.Note('C5', type='whole'))
435    >>> p1.append(note.Note('D5', type='whole'))
436    >>> p2.append(note.Note('C3', type='whole'))
437    >>> p2.append(note.Note('D3', type='whole'))
438    >>> p3 = stream.Part()
439    >>> p3.append(note.Note('F#4', type='whole'))
440    >>> p3.append(note.Note('G#4', type='whole'))
441    >>> s = stream.Score()
442    >>> s.insert(0, p1)
443    >>> s.insert(0, p2)
444    >>> s.insert(0, p3)
445    >>> staffGroup1 = layout.StaffGroup([p1, p2],
446    ...      name='Marimba', abbreviation='Mba.', symbol='brace')
447    >>> staffGroup1.barTogether = 'Mensurstrich'
448    >>> s.insert(0, staffGroup1)
449    >>> staffGroup2 = layout.StaffGroup([p3],
450    ...      name='Xylophone', abbreviation='Xyl.', symbol='bracket')
451    >>> s.insert(0, staffGroup2)
452    >>> #_DOCS_SHOW s.show()
453
454    .. image:: images/layout_StaffGroup_01.*
455        :width: 400
456
457    '''
458
459    def __init__(self, *arguments, **keywords):
460        super().__init__(*arguments, **keywords)
461
462        self.name = None  # if this group has a name
463        self.abbreviation = None
464        self._symbol = None  # can be bracket, line, brace, square
465        # determines if barlines are grouped through; this is group barline
466        # in musicxml
467        self._barTogether = True
468
469        if 'symbol' in keywords:
470            self.symbol = keywords['symbol']  # user property
471        if 'barTogether' in keywords:
472            self.barTogether = keywords['barTogether']  # user property
473        if 'name' in keywords:
474            self.name = keywords['name']  # user property
475        if 'abbreviation' in keywords:
476            self.name = keywords['abbreviation']  # user property
477
478    # --------------------------------------------------------------------------
479
480    def _getBarTogether(self):
481        return self._barTogether
482
483    def _setBarTogether(self, value):
484        if value is None:
485            pass  # do nothing for now; could set a default
486        elif value in ['yes', True]:
487            self._barTogether = True
488        elif value in ['no', False]:
489            self._barTogether = False
490        elif hasattr(value, 'lower') and value.lower() == 'mensurstrich':
491            self._barTogether = 'Mensurstrich'
492        else:
493            raise StaffGroupException(f'the bar together value {value} is not acceptable')
494
495    barTogether = property(_getBarTogether, _setBarTogether, doc='''
496        Get or set the barTogether value, with either Boolean values
497        or yes or no strings.  Or the string 'Mensurstrich' which
498        indicates barring between staves but not in staves.
499
500        Currently Mensurstrich i
501
502
503        >>> sg = layout.StaffGroup()
504        >>> sg.barTogether = 'yes'
505        >>> sg.barTogether
506        True
507        >>> sg.barTogether = 'Mensurstrich'
508        >>> sg.barTogether
509        'Mensurstrich'
510        ''')
511
512    def _getSymbol(self):
513        return self._symbol
514
515    def _setSymbol(self, value):
516        if value is None or str(value).lower() == 'none':
517            self._symbol = None
518        elif value.lower() in ['brace', 'line', 'bracket', 'square']:
519            self._symbol = value.lower()
520        else:
521            raise StaffGroupException(f'the symbol value {value} is not acceptable')
522
523    symbol = property(_getSymbol, _setSymbol, doc='''
524        Get or set the symbol value, with either Boolean values or yes or no strings.
525
526
527        >>> sg = layout.StaffGroup()
528        >>> sg.symbol = 'Brace'
529        >>> sg.symbol
530        'brace'
531        ''')
532
533
534# ---------------------------------------------------------------
535# Stream subclasses for layout
536
537def divideByPages(scoreIn, printUpdates=False, fastMeasures=False):
538    '''
539    Divides a score into a series of smaller scores according to page
540    breaks.  Only searches for PageLayout.isNew or SystemLayout.isNew
541    on the first part.  Returns a new `LayoutScore` object.
542
543    If fastMeasures is True, then the newly created System objects
544    do not have Clef signs, Key Signatures, or necessarily all the
545    applicable spanners in them.  On the other hand, the position
546    (on the page) information will be just as correct with
547    fastMeasures = True and it will run much faster on large scores
548    (because our spanner gathering algorithm is currently O(n^2);
549    something TODO: to fix.)
550
551
552    >>> lt = corpus.parse('demos/layoutTest.xml')
553    >>> len(lt.parts)
554    3
555    >>> len(lt.parts[0].getElementsByClass('Measure'))
556    80
557
558
559    Divide the score up into layout.Page objects
560
561    >>> layoutScore = layout.divideByPages(lt, fastMeasures=True)
562    >>> len(layoutScore.pages)
563    4
564    >>> lastPage = layoutScore.pages[-1]
565    >>> lastPage.measureStart
566    64
567    >>> lastPage.measureEnd
568    80
569
570    the layoutScore is a subclass of stream.Opus:
571
572    >>> layoutScore
573    <music21.layout.LayoutScore ...>
574    >>> 'Opus' in layoutScore.classes
575    True
576
577    Pages are subclasses of Opus also, since they contain Scores
578
579    >>> lastPage
580    <music21.layout.Page ...>
581    >>> 'Opus' in lastPage.classes
582    True
583
584
585    Each page now has Systems not parts.
586
587    >>> firstPage = layoutScore.pages[0]
588    >>> len(firstPage.systems)
589    4
590    >>> firstSystem = firstPage.systems[0]
591    >>> firstSystem.measureStart
592    1
593    >>> firstSystem.measureEnd
594    5
595
596    Systems are a subclass of Score:
597
598    >>> firstSystem
599    <music21.layout.System ...>
600    >>> isinstance(firstSystem, stream.Score)
601    True
602
603    Each System has staves (layout.Staff objects) not parts, though Staff is a subclass of Part
604
605    >>> secondStaff = firstSystem.staves[1]
606    >>> print(len(secondStaff.getElementsByClass('Measure')))
607    5
608    >>> secondStaff
609    <music21.layout.Staff ...>
610    >>> isinstance(secondStaff, stream.Part)
611    True
612    '''
613    def getRichSystemLayout(inner_allSystemLayouts):
614        '''
615        If there are multiple systemLayouts in an iterable (list or StreamIterator),
616        make a copy of the first one and get information from each successive one into
617        a rich system layout.
618        '''
619        richestSystemLayout = copy.deepcopy(inner_allSystemLayouts[0])
620        for sl in inner_allSystemLayouts[1:]:
621            for attribute in ('distance', 'topDistance', 'leftMargin', 'rightMargin'):
622                if (getattr(richestSystemLayout, attribute) is None
623                        and getattr(sl, attribute) is not None):
624                    setattr(richestSystemLayout, attribute, getattr(sl, attribute))
625        return richestSystemLayout
626
627    pageMeasureTuples = getPageRegionMeasureNumbers(scoreIn)
628    systemMeasureTuples = getSystemRegionMeasureNumbers(scoreIn)
629    firstMeasureNumber = pageMeasureTuples[0][0]
630    lastMeasureNumber = pageMeasureTuples[-1][1]
631
632    scoreLists = LayoutScore()
633    scoreLists.definesExplicitPageBreaks = True
634    scoreLists.definesExplicitSystemBreaks = True
635    scoreLists.measureStart = firstMeasureNumber
636    scoreLists.measureEnd = lastMeasureNumber
637    for el in scoreIn:
638        if not isinstance(el, stream.Part):
639            if 'ScoreLayout' in el.classes:
640                scoreLists.scoreLayout = el
641            scoreLists.insert(scoreIn.elementOffset(el), el)
642
643    pageNumber = 0
644    systemNumber = 0
645    scoreStaffNumber = 0
646
647    for pageStartM, pageEndM in pageMeasureTuples:
648        pageNumber += 1
649        if printUpdates is True:
650            print('updating page', pageNumber)
651        thisPage = Page()
652        thisPage.measureStart = pageStartM
653        thisPage.measureEnd = pageEndM
654        thisPage.pageNumber = pageNumber
655        if fastMeasures is True:
656            thisPageAll = scoreIn.measures(pageStartM, pageEndM, collect=[], gatherSpanners=False)
657        else:
658            thisPageAll = scoreIn.measures(pageStartM, pageEndM)
659        thisPage.systemStart = systemNumber + 1
660        for el in thisPageAll:
661            if not isinstance(el.classes and 'StaffGroup' not in el, stream.Part):
662                thisPage.insert(thisPageAll.elementOffset(el), el)
663        firstMeasureOfFirstPart = thisPageAll.parts.first().getElementsByClass('Measure').first()
664        for el in firstMeasureOfFirstPart:
665            if 'PageLayout' in el.classes:
666                thisPage.pageLayout = el
667
668        pageSystemNumber = 0
669        for systemStartM, systemEndM in systemMeasureTuples:
670            if systemStartM < pageStartM or systemEndM > pageEndM:
671                continue
672            systemNumber += 1  # global, not on this page...
673            pageSystemNumber += 1
674            if fastMeasures is True:
675                measureStacks = scoreIn.measures(systemStartM, systemEndM,
676                                                 collect=[],
677                                                 gatherSpanners=False)
678            else:
679                measureStacks = scoreIn.measures(systemStartM, systemEndM)
680            thisSystem = System()
681            thisSystem.systemNumber = systemNumber
682            thisSystem.pageNumber = pageNumber
683            thisSystem.pageSystemNumber = pageSystemNumber
684            thisSystem.mergeAttributes(measureStacks)
685            thisSystem.elements = measureStacks
686            thisSystem.measureStart = systemStartM
687            thisSystem.measureEnd = systemEndM
688
689            systemStaffNumber = 0
690
691            for p in list(thisSystem.parts):
692                scoreStaffNumber += 1
693                systemStaffNumber += 1
694
695                staffObject = Staff()
696                staffObject.mergeAttributes(p)
697                staffObject.scoreStaffNumber = scoreStaffNumber
698                staffObject.staffNumber = systemStaffNumber
699                staffObject.pageNumber = pageNumber
700                staffObject.pageSystemNumber = pageSystemNumber
701
702                staffObject.elements = p
703                thisSystem.replace(p, staffObject)
704                allStaffLayouts = p.recurse().getElementsByClass('StaffLayout')
705                if not allStaffLayouts:
706                    continue
707                # else:
708                staffObject.staffLayout = allStaffLayouts[0]
709                # if len(allStaffLayouts) > 1:
710                #    print('Got many staffLayouts')
711
712            allSystemLayouts = thisSystem.recurse().getElementsByClass('SystemLayout')
713            if len(allSystemLayouts) >= 2:
714                thisSystem.systemLayout = getRichSystemLayout(allSystemLayouts)
715            elif len(allSystemLayouts) == 1:
716                thisSystem.systemLayout = allSystemLayouts[0]
717            else:
718                thisSystem.systemLayout = None
719
720            thisPage.coreAppend(thisSystem)
721        thisPage.systemEnd = systemNumber
722        thisPage.coreElementsChanged()
723        scoreLists.coreAppend(thisPage)
724    scoreLists.coreElementsChanged()
725    return scoreLists
726
727
728def getPageRegionMeasureNumbers(scoreIn):
729    return getRegionMeasureNumbers(scoreIn, 'Page')
730
731
732def getSystemRegionMeasureNumbers(scoreIn):
733    return getRegionMeasureNumbers(scoreIn, 'System')
734
735
736def getRegionMeasureNumbers(scoreIn, region='Page'):
737    '''
738    get a list where each entry is a 2-tuplet whose first number
739    refers to the first measure on a page and whose second number
740    is the last measure on the page.
741    '''
742    if region == 'Page':
743        classesToReturn = ['PageLayout']
744    elif region == 'System':
745        classesToReturn = ['PageLayout', 'SystemLayout']
746    else:
747        raise ValueError('region must be one of Page or System')
748
749    firstPart = scoreIn.parts.first()
750    # first measure could be 1 or 0 (or something else)
751    allMeasures = firstPart.getElementsByClass('Measure')
752    firstMeasureNumber = allMeasures.first().number
753    lastMeasureNumber = allMeasures.last().number
754    measureStartList = [firstMeasureNumber]
755    measureEndList = []
756    allAppropriateLayout = firstPart.flatten().getElementsByClass(classesToReturn)
757
758    for pl in allAppropriateLayout:
759        plMeasureNumber = pl.measureNumber
760        if pl.isNew is False:
761            continue
762        if plMeasureNumber not in measureStartList:
763            # in case of firstMeasureNumber or system and page layout at same time.
764            measureStartList.append(plMeasureNumber)
765            measureEndList.append(plMeasureNumber - 1)
766    measureEndList.append(lastMeasureNumber)
767    measureList = list(zip(measureStartList, measureEndList))
768    return measureList
769
770
771class LayoutScore(stream.Opus):
772    '''
773    Designation that this Score is
774    divided into Pages, Systems, Staves (=Parts),
775    Measures, etc.
776
777    Used for computing location of notes, etc.
778
779    If the score does not change between calls to the various getPosition calls,
780    it is much faster as it uses a cache.
781    '''
782
783    def __init__(self, *args, **keywords):
784        super().__init__(*args, **keywords)
785        self.scoreLayout = None
786        self.measureStart = None
787        self.measureEnd = None
788
789    @property
790    def pages(self):
791        return self.getElementsByClass(Page)
792
793    def show(self, fmt=None, app=None, **keywords):
794        '''
795        Borrows stream.Score.show
796
797        >>> lp = layout.Page()
798        >>> ls = layout.LayoutScore()
799        >>> ls.append(lp)
800        >>> ls.show('text')
801        {0.0} <music21.layout.Page p.1>
802        <BLANKLINE>
803        '''
804        return stream.Score.show(self, fmt=fmt, app=app, **keywords)
805
806    def getPageAndSystemNumberFromMeasureNumber(self, measureNumber):
807        '''
808        Given a layoutScore from divideByPages and a measureNumber returns a tuple
809        of (pageId, systemId).  Note that pageId is probably one less than the page number,
810        assuming that the first page number is 1, the pageId for the first page will be 0.
811
812        Similarly, the first systemId on each page will be 0
813
814
815        >>> lt = corpus.parse('demos/layoutTest.xml')
816        >>> l = layout.divideByPages(lt, fastMeasures=True)
817        >>> l.getPageAndSystemNumberFromMeasureNumber(80)
818        (3, 3)
819        '''
820        if 'pageAndSystemNumberFromMeasureNumbers' not in self._cache:
821            self._cache['pageAndSystemNumberFromMeasureNumbers'] = {}
822        dataCache = self._cache['pageAndSystemNumberFromMeasureNumbers']
823
824        if measureNumber in dataCache:
825            return dataCache[measureNumber]
826
827        foundPage = None
828        foundPageId = None
829
830        for pageId, thisPage in enumerate(self.pages):
831            if measureNumber < thisPage.measureStart or measureNumber > thisPage.measureEnd:
832                continue
833            foundPage = thisPage
834            foundPageId = pageId
835            break
836
837        if foundPage is None:
838            raise LayoutException('Cannot find this measure on any page!')
839
840        foundSystem = None
841        foundSystemId = None
842        for systemId, thisSystem in enumerate(foundPage.systems):
843            if measureNumber < thisSystem.measureStart or measureNumber > thisSystem.measureEnd:
844                continue
845            foundSystem = thisSystem
846            foundSystemId = systemId
847            break
848
849        if foundSystem is None:
850            raise LayoutException("that's strange, this measure was supposed to be on this page, "
851                                  + "but I couldn't find it anywhere!")
852        dataCache[measureNumber] = (foundPageId, foundSystemId)
853        return (foundPageId, foundSystemId)
854
855    def getMarginsAndSizeForPageId(self, pageId):
856        '''
857        return a namedtuple of (top, left, bottom, right, width, height)
858        margins for a given pageId in tenths
859
860        Default of (100, 100, 100, 100, 850, 1100) if undefined
861
862
863        >>> #_DOCS_SHOW g = corpus.parse('luca/gloria')
864        >>> #_DOCS_SHOW m22 = g.parts[0].getElementsByClass('Measure')[22]
865        >>> #_DOCS_SHOW m22.getElementsByClass('PageLayout').first().leftMargin = 204.0
866        >>> #_DOCS_SHOW gl = layout.divideByPages(g)
867        >>> #_DOCS_SHOW gl.getMarginsAndSizeForPageId(1)
868        >>> layout.PageSize(171.0, 204.0, 171.0, 171.0, 1457.0, 1886.0) #_DOCS_HIDE
869        PageSize(top=171.0, left=204.0, right=171.0, bottom=171.0, width=1457.0, height=1886.0)
870        '''
871        if 'marginsAndSizeForPageId' not in self._cache:
872            self._cache['marginsAndSizeForPageId'] = {}
873        dataCache = self._cache['marginsAndSizeForPageId']
874        if pageId in dataCache:
875            return dataCache[pageId]
876
877        # define defaults
878        pageMarginTop = 100
879        pageMarginLeft = 100
880        pageMarginRight = 100
881        pageMarginBottom = 100
882        pageWidth = 850
883        pageHeight = 1100
884
885        thisPage = self.pages[pageId]
886
887        # override defaults with scoreLayout
888        if self.scoreLayout is not None:
889            scl = self.scoreLayout
890            if scl.pageLayout is not None:
891                pl = scl.pageLayout
892                if pl.pageWidth is not None:
893                    pageWidth = pl.pageWidth
894                if pl.pageHeight is not None:
895                    pageHeight = pl.pageHeight
896                if pl.topMargin is not None:
897                    pageMarginTop = pl.topMargin
898                if pl.leftMargin is not None:
899                    pageMarginLeft = pl.leftMargin
900                if pl.rightMargin is not None:
901                    pageMarginRight = pl.rightMargin
902                if pl.bottomMargin is not None:
903                    pageMarginBottom = pl.bottomMargin
904
905        # override global information with page specific pageLayout
906        if thisPage.pageLayout is not None:
907            pl = thisPage.pageLayout
908            if pl.pageWidth is not None:
909                pageWidth = pl.pageWidth
910            if pl.pageHeight is not None:
911                pageHeight = pl.pageHeight
912            if pl.topMargin is not None:
913                pageMarginTop = pl.topMargin
914            if pl.leftMargin is not None:
915                pageMarginLeft = pl.leftMargin
916            if pl.rightMargin is not None:
917                pageMarginRight = pl.rightMargin
918            if pl.bottomMargin is not None:
919                pageMarginBottom = pl.bottomMargin
920
921        dataTuple = PageSize(pageMarginTop, pageMarginLeft, pageMarginBottom, pageMarginRight,
922                             pageWidth, pageHeight)
923        dataCache[pageId] = dataTuple
924        return dataTuple
925
926    def getPositionForSystem(self, pageId, systemId):
927        '''
928        first systems on a page use a different positioning.
929
930        returns a Named tuple of the (top, left, right, and bottom) where each unit is
931        relative to the page margins
932
933        N.B. right is NOT the width -- it is different.  It is the offset to the right margin.
934        weird, inconsistent, but most useful...bottom is the hard part to compute...
935
936
937        >>> lt = corpus.parse('demos/layoutTestMore.xml')
938        >>> ls = layout.divideByPages(lt, fastMeasures = True)
939        >>> ls.getPositionForSystem(0, 0)
940        SystemSize(top=211.0, left=70.0, right=0.0, bottom=696.0)
941        >>> ls.getPositionForSystem(0, 1)
942        SystemSize(top=810.0, left=0.0, right=0.0, bottom=1173.0)
943        >>> ls.getPositionForSystem(0, 2)
944        SystemSize(top=1340.0, left=67.0, right=92.0, bottom=1610.0)
945        >>> ls.getPositionForSystem(0, 3)
946        SystemSize(top=1724.0, left=0.0, right=0.0, bottom=2030.0)
947        >>> ls.getPositionForSystem(0, 4)
948        SystemSize(top=2144.0, left=0.0, right=0.0, bottom=2583.0)
949        '''
950        if 'positionForSystem' not in self._cache:
951            self._cache['positionForSystem'] = {}
952        positionForSystemCache = self._cache['positionForSystem']
953        cacheKey = f'{pageId}-{systemId}'
954        if cacheKey in positionForSystemCache:
955            return positionForSystemCache[cacheKey]
956
957        if pageId == 0 and systemId == 4:
958            pass
959
960        leftMargin = 0
961        rightMargin = 0
962        # no top or bottom margins
963
964        # distance from previous
965        previousDistance = 0
966
967        # override defaults with scoreLayout
968        if self.scoreLayout is not None:
969            scl = self.scoreLayout
970            if scl.systemLayout is not None:
971                sl = scl.systemLayout
972                if sl.leftMargin is not None:
973                    leftMargin = sl.leftMargin
974                if sl.rightMargin is not None:
975                    rightMargin = sl.rightMargin
976                if systemId == 0:
977                    if sl.topDistance is not None:
978                        previousDistance = sl.topDistance
979                else:
980                    if sl.distance is not None:
981                        previousDistance = sl.distance
982
983        # override global information with system specific pageLayout
984        thisSystem = self.pages[pageId].systems[systemId]
985
986        if thisSystem.systemLayout is not None:
987            sl = thisSystem.systemLayout
988            if sl.leftMargin is not None:
989                leftMargin = sl.leftMargin
990            if sl.rightMargin is not None:
991                rightMargin = sl.rightMargin
992            if systemId == 0:
993                if sl.topDistance is not None:
994                    previousDistance = sl.topDistance
995            else:
996                if sl.distance is not None:
997                    previousDistance = sl.distance
998
999        if systemId > 0:
1000            lastSystemDimensions = self.getPositionForSystem(pageId, systemId - 1)
1001            bottomOfLastSystem = lastSystemDimensions.bottom
1002        else:
1003            bottomOfLastSystem = 0
1004
1005        numStaves = len(thisSystem.staves)
1006        lastStaff = numStaves - 1  #
1007        unused_systemStart, systemHeight = self.getPositionForStaff(pageId, systemId, lastStaff)
1008
1009        top = previousDistance + bottomOfLastSystem
1010        bottom = top + systemHeight
1011        dataTuple = SystemSize(float(top), float(leftMargin), float(rightMargin), float(bottom))
1012        positionForSystemCache[cacheKey] = dataTuple
1013        return dataTuple
1014
1015    def getPositionForStaff(self, pageId, systemId, staffId):
1016        '''
1017        return a tuple of (top, bottom) for a staff, specified by a given pageId,
1018        systemId, and staffId in tenths of a staff-space.
1019
1020        This distance is specified with respect to the top of the system.
1021
1022        Staff scaling (<staff-details> in musicxml inside an <attributes> object) is
1023        taken into account, but not non 5-line staves.  Thus a normally sized staff
1024        is always of height 40 (4 spaces of 10-tenths each)
1025
1026        >>> lt = corpus.parse('demos/layoutTest.xml')
1027        >>> ls = layout.divideByPages(lt, fastMeasures=True)
1028
1029        The first staff (staff 0) of each page/system always begins at height 0 and should end at
1030        height 40 if it is a 5-line staff (not taken into account) with no staffSize changes
1031
1032        >>> ls.getPositionForStaff(0, 0, 0)
1033        (0.0, 40.0)
1034        >>> ls.getPositionForStaff(1, 0, 0)
1035        (0.0, 40.0)
1036
1037        The second staff (staff 1) begins at the end of staff 0 (40.0) +
1038        the appropriate staffDistance
1039        and adds the height of the staff.  Staff 1 here has a size of 80 which means
1040        80% of the normal staff size.  40 * 0.8 = 32.0:
1041
1042        >>> ls.getPositionForStaff(0, 0, 1)
1043        (133.0, 165.0)
1044
1045        The third staff (staff 2) begins after the second staff (staff 1) but is a normal
1046        size staff
1047
1048        >>> ls.getPositionForStaff(0, 0, 2)
1049        (266.0, 306.0)
1050
1051        The first staff (staff 0) of the second system (system 1) also begins at 0
1052        and as a normally-sized staff, has height of 40:
1053
1054        >>> ls.getPositionForStaff(0, 1, 0)
1055        (0.0, 40.0)
1056
1057        The spacing between the staves has changed in the second system, but the
1058        staff height has not:
1059
1060        >>> ls.getPositionForStaff(0, 1, 1)
1061        (183.0, 215.0)
1062        >>> ls.getPositionForStaff(0, 1, 2)
1063        (356.0, 396.0)
1064
1065        In the third system (system 2), the staff distance reverts to the distance
1066        of system 0, but the staffSize is now 120 or 48 tenths (40 * 1.2 = 48)
1067
1068        >>> ls.getPositionForStaff(0, 2, 1)
1069        (117.0, 165.0)
1070
1071        Page 1 (0), System 4 (3), Staff 2 (1) is a hidden ("optimized") system.
1072        Thus its staffLayout notes this:
1073
1074        >>> staffLayout031 = ls.pages[0].systems[3].staves[1].staffLayout
1075        >>> staffLayout031
1076        <music21.layout.StaffLayout distance None, staffNumber None, staffSize 80, staffLines None>
1077        >>> staffLayout031.hidden
1078        True
1079
1080        Thus, the position for this staff will have the same top and bottom, and the
1081        position for the next staff will have the same top as the previous staff:
1082
1083        >>> ls.getPositionForStaff(0, 3, 0)
1084        (0.0, 40.0)
1085        >>> ls.getPositionForStaff(0, 3, 1)
1086        (40.0, 40.0)
1087        >>> ls.getPositionForStaff(0, 3, 2)
1088        (133.0, 173.0)
1089
1090
1091        Tests for a score with PartStaff objects:
1092        >>> lt = corpus.parse('demos/layoutTestMore.xml')
1093        >>> ls = layout.divideByPages(lt, fastMeasures = True)
1094        >>> ls.getPositionForStaff(0, 0, 0)
1095        (0.0, 40.0)
1096        >>> ls.getPositionForStaff(0, 0, 1)
1097        (133.0, 173.0)
1098        >>> ls.getPositionForStaff(0, 0, 2)
1099        (235.0, 275.0)
1100
1101        >>> ls.getPositionForStaff(0, 2, 0)
1102        (0.0, 40.0)
1103        >>> ls.getPositionForStaff(0, 2, 1)
1104        (40.0, 40.0)
1105        >>> ls.getPositionForStaff(0, 2, 2)
1106        (40.0, 40.0)
1107
1108        System 4 has the top staff hidden, which has been causing problems:
1109
1110        >>> ls.getPositionForStaff(0, 4, 0)
1111        (0.0, 0.0)
1112        >>> ls.getPositionForStaff(0, 4, 1)
1113        (0.0, 40.0)
1114        '''
1115        # if staffId == 99:
1116        #    staffId = 1
1117        if 'positionForStaff' not in self._cache:
1118            self._cache['positionForStaff'] = {}
1119        positionForStaffCache = self._cache['positionForStaff']
1120        cacheKey = f'{pageId}-{systemId}-{staffId}'
1121        if cacheKey in positionForStaffCache:
1122            return positionForStaffCache[cacheKey]
1123
1124        hiddenStaff = self.getStaffHiddenAttribute(pageId, systemId, staffId)  # False
1125        if hiddenStaff is not True:
1126            staffDistanceFromPrevious = self.getStaffDistanceFromPrevious(pageId, systemId, staffId)
1127            staffHeight = self.getStaffSizeFromLayout(pageId, systemId, staffId)
1128        else:  # hiddenStaff is True
1129            staffHeight = 0.0
1130            staffDistanceFromPrevious = 0.0
1131
1132        if staffId > 0:
1133            unused_previousStaffTop, previousStaffBottom = self.getPositionForStaff(
1134                pageId, systemId, staffId - 1)
1135        else:
1136            previousStaffBottom = 0
1137
1138        staffDistanceFromStart = staffDistanceFromPrevious + previousStaffBottom
1139        staffBottom = staffDistanceFromStart + staffHeight
1140
1141        dataTuple = (staffDistanceFromStart, staffBottom)
1142        positionForStaffCache[cacheKey] = dataTuple
1143        return dataTuple
1144
1145    def getStaffDistanceFromPrevious(self, pageId, systemId, staffId):
1146        '''
1147        return the distance of this staff from the previous staff in the same system
1148
1149        for staffId = 0, this is always 0.0
1150
1151        TODO:tests, now that this is out from previous
1152        '''
1153        if staffId == 0:
1154            return 0.0
1155
1156        if 'distanceFromPrevious' not in self._cache:
1157            self._cache['distanceFromPrevious'] = {}
1158        positionForStaffCache = self._cache['distanceFromPrevious']
1159        cacheKey = f'{pageId}-{systemId}-{staffId}'
1160        if cacheKey in positionForStaffCache:
1161            return positionForStaffCache[cacheKey]
1162
1163        # if this is the first non-hidden staff in the score then also return 0
1164        foundVisibleStaff = False
1165        i = staffId - 1
1166        while i >= 0:
1167            hiddenStatus = self.getStaffHiddenAttribute(pageId, systemId, i)
1168            if hiddenStatus is False:
1169                foundVisibleStaff = True
1170                break
1171            else:
1172                i = i - 1
1173        if foundVisibleStaff is False:
1174            positionForStaffCache[cacheKey] = 0.0
1175            return 0.0
1176
1177        # nope, not first staff or first visible staff...
1178
1179        staffDistanceFromPrevious = 60.0  # sensible default?
1180
1181        if self.scoreLayout is not None:
1182            scl = self.scoreLayout
1183            if scl.staffLayoutList:
1184                for slTemp in scl.staffLayoutList:
1185                    distanceTemp = slTemp.distance
1186                    if distanceTemp is not None:
1187                        staffDistanceFromPrevious = distanceTemp
1188                        break
1189
1190        # override global information with staff specific pageLayout
1191        thisStaff = self.pages[pageId].systems[systemId].staves[staffId]
1192        firstMeasureOfStaff = thisStaff.getElementsByClass('Measure').first()
1193        if firstMeasureOfStaff is None:
1194            firstMeasureOfStaff = stream.Stream()
1195            environLocal.warn(
1196                f'No measures found in pageId {pageId}, systemId {systemId}, staffId {staffId}'
1197            )
1198
1199        allStaffLayouts = firstMeasureOfStaff.getElementsByClass('StaffLayout')
1200        if allStaffLayouts:
1201            # print('Got staffLayouts: ')
1202            for slTemp in allStaffLayouts:
1203                distanceTemp = slTemp.distance
1204                if distanceTemp is not None:
1205                    staffDistanceFromPrevious = distanceTemp
1206                    break
1207
1208        positionForStaffCache[cacheKey] = staffDistanceFromPrevious
1209        return staffDistanceFromPrevious
1210
1211    def getStaffSizeFromLayout(self, pageId, systemId, staffId):
1212        '''
1213        Get the currently active staff-size for a given pageId, systemId, and staffId.
1214
1215        Note that this does not take into account the hidden state of the staff, which
1216        if True makes the effective size 0.0 -- see getStaffHiddenAttribute
1217
1218        >>> lt = corpus.parse('demos/layoutTest.xml')
1219        >>> ls = layout.divideByPages(lt, fastMeasures=True)
1220        >>> ls.getStaffSizeFromLayout(0, 0, 0)
1221        40.0
1222        >>> ls.getStaffSizeFromLayout(0, 0, 1)
1223        32.0
1224        >>> ls.getStaffSizeFromLayout(0, 0, 2)
1225        40.0
1226        >>> ls.getStaffSizeFromLayout(0, 1, 1)
1227        32.0
1228        >>> ls.getStaffSizeFromLayout(0, 2, 1)
1229        48.0
1230        >>> ls.getStaffSizeFromLayout(0, 3, 1)
1231        32.0
1232        '''
1233        if 'staffSize' not in self._cache:
1234            self._cache['staffSize'] = {}
1235        staffSizeCache = self._cache['staffSize']
1236        cacheKey = f'{pageId}-{systemId}-{staffId}'
1237        if cacheKey in staffSizeCache:
1238            return staffSizeCache[cacheKey]
1239
1240        thisStaff = self.pages[pageId].systems[systemId].staves[staffId]
1241        firstMeasureOfStaff = thisStaff.getElementsByClass('Measure').first()
1242        if firstMeasureOfStaff is None:
1243            firstMeasureOfStaff = stream.Stream()
1244            environLocal.warn(
1245                f'No measures found in pageId {pageId}, systemId {systemId}, staffId {staffId}'
1246            )
1247
1248        numStaffLines = 5  # TODO: should be taken from staff attributes
1249        numSpaces = numStaffLines - 1
1250        staffSizeBase = numSpaces * 10.0
1251        staffSizeDefinedLocally = False
1252
1253        staffSize = staffSizeBase
1254
1255        allStaffLayouts = list(firstMeasureOfStaff.getElementsByClass('StaffLayout'))
1256        if allStaffLayouts:
1257            # print('Got staffLayouts: ')
1258            staffLayoutObj = allStaffLayouts[0]
1259            if staffLayoutObj.staffSize is not None:
1260                staffSize = staffSizeBase * (staffLayoutObj.staffSize / 100.0)
1261                # print(f'Got staffHeight of {staffHeight} for partId {partId}')
1262                staffSizeDefinedLocally = True
1263
1264        if staffSizeDefinedLocally is False:
1265            previousPageId, previousSystemId = self.getSystemBeforeThis(pageId, systemId)
1266            if previousPageId is None:
1267                staffSize = staffSizeBase
1268            else:
1269                staffSize = self.getStaffSizeFromLayout(previousPageId, previousSystemId, staffId)
1270
1271        staffSize = float(staffSize)
1272        staffSizeCache[cacheKey] = staffSize
1273        return staffSize
1274
1275    def getStaffHiddenAttribute(self, pageId, systemId, staffId):
1276        '''
1277        returns the staffLayout.hidden attribute for a staffId, or if it is not
1278        defined, recursively search through previous staves until one is found.
1279
1280
1281        >>> lt = corpus.parse('demos/layoutTestMore.xml')
1282        >>> ls = layout.divideByPages(lt, fastMeasures = True)
1283        >>> ls.getStaffHiddenAttribute(0, 0, 0)
1284        False
1285        >>> ls.getStaffHiddenAttribute(0, 0, 1)
1286        False
1287        >>> ls.getStaffHiddenAttribute(0, 1, 1)
1288        True
1289        >>> ls.getStaffHiddenAttribute(0, 2, 1)
1290        True
1291        >>> ls.getStaffHiddenAttribute(0, 3, 1)
1292        False
1293        '''
1294        if 'staffHiddenAttribute' not in self._cache:
1295            self._cache['staffHiddenAttribute'] = {}
1296
1297        staffHiddenCache = self._cache['staffHiddenAttribute']
1298        cacheKey = f'{pageId}-{systemId}-{staffId}'
1299        if cacheKey in staffHiddenCache:
1300            return staffHiddenCache[cacheKey]
1301
1302        thisStaff = self.pages[pageId].systems[systemId].staves[staffId]
1303
1304        staffLayoutObject = None
1305        allStaffLayoutObjects = list(thisStaff.flatten().getElementsByClass('StaffLayout'))
1306        if allStaffLayoutObjects:
1307            staffLayoutObject = allStaffLayoutObjects[0]
1308        if staffLayoutObject is None or staffLayoutObject.hidden is None:
1309            previousPageId, previousSystemId = self.getSystemBeforeThis(pageId, systemId)
1310            if previousPageId is None:
1311                hiddenTag = False
1312            else:
1313                hiddenTag = self.getStaffHiddenAttribute(previousPageId, previousSystemId, staffId)
1314        else:
1315            hiddenTag = staffLayoutObject.hidden
1316
1317        staffHiddenCache[cacheKey] = hiddenTag
1318        return hiddenTag
1319
1320    def getSystemBeforeThis(self, pageId, systemId):
1321        # noinspection PyShadowingNames
1322        '''
1323        given a pageId and systemId, get the (pageId, systemId) for the previous system.
1324
1325        return (None, None) if it's the first system on the first page
1326
1327        This test score has five systems on the first page,
1328        three on the second, and two on the third
1329
1330        >>> lt = corpus.parse('demos/layoutTestMore.xml')
1331        >>> ls = layout.divideByPages(lt, fastMeasures = True)
1332        >>> systemId = 1
1333        >>> pageId = 2  # last system, last page
1334        >>> while systemId is not None:
1335        ...    pageId, systemId = ls.getSystemBeforeThis(pageId, systemId)
1336        ...    (pageId, systemId)
1337        (2, 0) (1, 2) (1, 1) (1, 0) (0, 4) (0, 3) (0, 2) (0, 1) (0, 0) (None, None)
1338        '''
1339        if systemId > 0:
1340            return pageId, systemId - 1
1341        else:
1342            if pageId == 0:
1343                return (None, None)
1344            previousPageId = pageId - 1
1345            numSystems = len(self.pages[previousPageId].systems)
1346            return previousPageId, numSystems - 1
1347
1348    def getPositionForStaffMeasure(self, staffId, measureNumber, returnFormat='tenths'):
1349        '''
1350        Given a layoutScore from divideByPages, a staffId, and a measureNumber,
1351        returns a tuple of ((top, left), (bottom, right), pageId)
1352        allowing an exact position for the measure on the page.
1353        If returnFormat is "tenths", then it will be returned in tenths.
1354
1355        If returnFormat is "float", returns each as a number from 0 to 1 where 0 is the
1356        top or left of the page, and 1 is the bottom or right of the page.
1357
1358
1359        >>> lt = corpus.parse('demos/layoutTest.xml')
1360        >>> ls = layout.divideByPages(lt, fastMeasures = True)
1361
1362        The first measure of staff one begins at 336 tenths from the top (125 for the
1363        margin top and 211 for the top-staff-distance).  It begins 170.0 from the
1364        left (100 for the page-margin-left, 70 for staff-margin-left).  It ends
1365        40.0 below that (staffHeight) and 247.0 to the right (measure width)
1366
1367        >>> ls.getPositionForStaffMeasure(0, 1)
1368        ((336.0, 170.0), (376.0, 417.0), 0)
1369
1370        The other staves for the same measure are below this one:
1371
1372        >>> ls.getPositionForStaffMeasure(1, 1)
1373        ((469.0, 170.0), (501.0, 417.0), 0)
1374        >>> ls.getPositionForStaffMeasure(2, 1)
1375        ((602.0, 170.0), (642.0, 417.0), 0)
1376
1377
1378        If float is requested for returning, then the numbers are the fraction of
1379        the distance across the page.
1380
1381        >>> ls.getPositionForStaffMeasure(0, 1, returnFormat='float')
1382        ((0.152..., 0.0996...), (0.170..., 0.244...), 0)
1383
1384
1385        Moving over the page boundary:
1386
1387        >>> ls.getPositionForStaffMeasure(0, 23)
1388        ((1703.0, 1345.0), (1743.0, 1606.0), 0)
1389        >>> ls.getPositionForStaffMeasure(1, 23)  # hidden
1390        ((1743.0, 1345.0), (1743.0, 1606.0), 0)
1391        >>> ls.getPositionForStaffMeasure(0, 24)
1392        ((195.0, 100.0), (235.0, 431.0), 1)
1393        >>> ls.getPositionForStaffMeasure(1, 24)
1394        ((328.0, 100.0), (360.0, 431.0), 1)
1395        '''
1396        if 'positionForPartMeasure' not in self._cache:
1397            self._cache['positionForPartMeasure'] = {}
1398        positionForPartMeasureCache = self._cache['positionForPartMeasure']
1399        if measureNumber not in positionForPartMeasureCache:
1400            positionForPartMeasureCache[measureNumber] = {}
1401        dataCache = positionForPartMeasureCache[measureNumber]
1402        if staffId in dataCache:
1403            return dataCache[staffId]
1404
1405        pageId, systemId = self.getPageAndSystemNumberFromMeasureNumber(measureNumber)
1406
1407        startXMeasure, endXMeasure = self.measurePositionWithinSystem(
1408            measureNumber, pageId, systemId)
1409        staffTop, staffBottom = self.getPositionForStaff(pageId, systemId, staffId)
1410        systemPos = self.getPositionForSystem(pageId, systemId)
1411        systemTop = systemPos.top
1412        systemLeft = systemPos.left
1413        pageSize = self.getMarginsAndSizeForPageId(pageId)
1414
1415        top = pageSize.top + systemTop + staffTop
1416        left = pageSize.left + systemLeft + startXMeasure
1417        bottom = pageSize.top + systemTop + staffBottom
1418        right = pageSize.left + systemLeft + endXMeasure
1419        pageWidth = pageSize.width
1420        pageHeight = pageSize.height
1421
1422        dataTuple = None
1423        if returnFormat == 'tenths':
1424            dataTuple = ((top, left), (bottom, right), pageId)
1425        else:
1426            pageWidth = float(pageWidth)
1427            pageHeight = float(pageHeight)
1428            topRatio = float(top) / pageHeight
1429            leftRatio = float(left) / pageWidth
1430            bottomRatio = float(bottom) / pageHeight
1431            rightRatio = float(right) / pageWidth
1432            dataTuple = ((topRatio, leftRatio), (bottomRatio, rightRatio), pageId)
1433
1434        dataCache[staffId] = dataTuple
1435        return dataTuple
1436        # return self.getPositionForStaffIdSystemIdPageIdMeasure(
1437        #    staffId, systemId, pageId, measureNumber, returnFormat)
1438
1439    def measurePositionWithinSystem(self, measureNumber, pageId=None, systemId=None):
1440        '''
1441        Given a measure number, find the start and end X positions (with respect to
1442        the system margins) for the measure.
1443
1444        if pageId and systemId are given, then it will speed up the search. But not necessary
1445
1446        no staffId is needed since (at least for now) all measures begin and end at the same
1447        X position
1448
1449
1450        >>> l = corpus.parse('demos/layoutTest.xml')
1451        >>> ls = layout.divideByPages(l, fastMeasures = True)
1452        >>> ls.measurePositionWithinSystem(1, 0, 0)
1453        (0.0, 247.0)
1454        >>> ls.measurePositionWithinSystem(2, 0, 0)
1455        (247.0, 544.0)
1456        >>> ls.measurePositionWithinSystem(3, 0, 0)
1457        (544.0, 841.0)
1458
1459        Measure positions reset at the start of a new system
1460
1461        >>> ls.measurePositionWithinSystem(6)
1462        (0.0, 331.0)
1463        >>> ls.measurePositionWithinSystem(7)
1464        (331.0, 549.0)
1465        '''
1466        if pageId is None or systemId is None:
1467            pageId, systemId = self.getPageAndSystemNumberFromMeasureNumber(measureNumber)
1468
1469        thisSystem = self.pages[pageId].systems[systemId]
1470        startOffset = 0.0
1471        width = None
1472        thisSystemStaves = thisSystem.staves
1473        measureStream = thisSystemStaves[0].getElementsByClass('Measure')
1474        for i, m in enumerate(measureStream):
1475            currentWidth = m.layoutWidth
1476            if currentWidth is None:
1477                # first system is hidden, thus has no width information
1478                for j in range(1, len(thisSystemStaves)):
1479                    searchOtherStaffForWidth = thisSystemStaves[j]
1480                    searchIter = searchOtherStaffForWidth.iter()
1481                    searchOtherStaffMeasure = searchIter.getElementsByClass('Measure')[i]
1482                    if searchOtherStaffMeasure.layoutWidth is not None:
1483                        currentWidth = searchOtherStaffMeasure.layoutWidth
1484                        break
1485            if currentWidth is None:
1486                # error mode? throw error? or assume default width?  Let's do the latter for now
1487                environLocal.warn(
1488                    f'Could not get width for measure {m.number}, using default of 300')
1489                currentWidth = 300.0
1490            else:
1491                currentWidth = float(currentWidth)
1492            if m.number == measureNumber:
1493                width = currentWidth
1494                break
1495            else:
1496                startOffset += currentWidth
1497
1498        return startOffset, startOffset + width
1499
1500    def getAllMeasurePositionsInDocument(self, returnFormat='tenths', printUpdates=False):
1501        '''
1502        returns a list of dictionaries, where each dictionary gives the measure number
1503        and other information, etc. in the document.
1504
1505
1506        # >>> g = corpus.parse('luca/gloria')
1507        # >>> gl = layout.divideByPages(g)
1508        # >>> gl.getAllMeasurePositionsInDocument()
1509        '''
1510        numStaves = len(self.pages[0].systems[0].staves)
1511        allRetInfo = []
1512        for mNum in range(self.measureStart, self.measureEnd + 1):
1513            if printUpdates is True:  # so fast now that it's not needed
1514                print('Doing measure ', mNum)
1515            mList = []
1516            for staffNum in range(numStaves):
1517                tupleInfo = self.getPositionForStaffMeasure(staffNum, mNum, returnFormat)
1518                infoDict = {
1519                    'measureNumberActual': mNum,
1520                    'measureNumber': mNum - 1,
1521                    'staffNumber': staffNum,
1522                    'top': tupleInfo[0][0],
1523                    'left': tupleInfo[0][1],
1524                    'bottom': tupleInfo[1][0],
1525                    'right': tupleInfo[1][1],
1526                    'pageNumber': tupleInfo[2],
1527                }
1528                mList.append(infoDict)
1529            allRetInfo.append(mList)
1530        return allRetInfo
1531
1532
1533class Page(stream.Opus):
1534    '''
1535    Designation that all the music in this Stream
1536    belongs on a single notated page.
1537    '''
1538
1539    def __init__(self, *args, **keywords):
1540        super().__init__(*args, **keywords)
1541        self.pageNumber = 1
1542        self.measureStart = None
1543        self.measureEnd = None
1544        self.systemStart = None
1545        self.systemEnd = None
1546        self.pageLayout = None
1547
1548    def _reprInternal(self):
1549        return f'p.{self.pageNumber}'
1550
1551    @property
1552    def systems(self):
1553        return self.getElementsByClass(System)
1554
1555    def show(self, fmt=None, app=None, **keywords):
1556        '''
1557        Borrows stream.Score.show
1558
1559        >>> ls = layout.System()
1560        >>> lp = layout.Page()
1561        >>> lp.append(ls)
1562        >>> lp.show('text')
1563        {0.0} <music21.layout.System 0: p.0, sys.0>
1564        <BLANKLINE>
1565        '''
1566        return stream.Score.show(self, fmt=fmt, app=app, **keywords)
1567
1568
1569
1570class System(stream.Score):
1571    '''
1572    Designation that all the music in this Stream
1573    belongs on a single notated system.
1574    '''
1575
1576    def __init__(self, *args, **keywords):
1577        super().__init__(*args, **keywords)
1578        self.systemNumber = 0
1579
1580        self.pageNumber = 0
1581        self.pageSystemNumber = 0
1582
1583        self.systemLayout = None
1584        self.measureStart = None
1585        self.measureEnd = None
1586
1587    def _reprInternal(self):
1588        return f'{self.systemNumber}: p.{self.pageNumber}, sys.{self.pageSystemNumber}'
1589
1590    @property
1591    def staves(self):
1592        return self.getElementsByClass(Staff)
1593
1594
1595class Staff(stream.Part):
1596    '''
1597    Designation that all the music in this Stream
1598    belongs on a single Staff.
1599    '''
1600
1601    def __init__(self, *args, **keywords):
1602        super().__init__(*args, **keywords)
1603        self.staffNumber = 1  # number in this system NOT GLOBAL
1604
1605        self.scoreStaffNumber = 0
1606        self.pageNumber = 0
1607        self.pageSystemNumber = 0
1608
1609        self.optimized = 0
1610        self.height = None  # None = undefined
1611        self.inheritedHeight = None
1612        self.staffLayout = None
1613
1614    def _reprInternal(self):
1615        return '{0}: p.{1}, sys.{2}, st.{3}'.format(self.scoreStaffNumber,
1616                                                    self.pageNumber,
1617                                                    self.pageSystemNumber,
1618                                                    self.staffNumber)
1619
1620
1621_DOC_ORDER = [ScoreLayout, PageLayout, SystemLayout, StaffLayout, LayoutBase,
1622              LayoutScore, Page, System, Staff]
1623# ------------------------------------------------------------------------------
1624
1625
1626class Test(unittest.TestCase):
1627
1628    def testBasic(self):
1629        from music21 import note
1630        from music21.musicxml import m21ToXml
1631        s = stream.Stream()
1632
1633        for i in range(1, 11):
1634            m = stream.Measure()
1635            m.number = i
1636            n = note.Note()
1637            m.append(n)
1638            s.append(m)
1639
1640        sl = SystemLayout()
1641        # sl.isNew = True  # this should not be on first system
1642        # as this causes all subsequent margins to be distorted
1643        sl.leftMargin = 300
1644        sl.rightMargin = 300
1645        s.getElementsByClass('Measure')[0].insert(0, sl)
1646
1647        sl = SystemLayout()
1648        sl.isNew = True
1649        sl.leftMargin = 200
1650        sl.rightMargin = 200
1651        sl.distance = 40
1652        s.getElementsByClass('Measure')[2].insert(0, sl)
1653
1654        sl = SystemLayout()
1655        sl.isNew = True
1656        sl.leftMargin = 220
1657        s.getElementsByClass('Measure')[4].insert(0, sl)
1658
1659        sl = SystemLayout()
1660        sl.isNew = True
1661        sl.leftMargin = 60
1662        sl.rightMargin = 300
1663        sl.distance = 200
1664        s.getElementsByClass('Measure')[6].insert(0, sl)
1665
1666        sl = SystemLayout()
1667        sl.isNew = True
1668        sl.leftMargin = 0
1669        sl.rightMargin = 0
1670        s.getElementsByClass('Measure')[8].insert(0, sl)
1671
1672        # systemLayoutList = s[music21.layout.SystemLayout]
1673        # self.assertEqual(len(systemLayoutList), 4)
1674
1675        # s.show()
1676        unused_raw = m21ToXml.GeneralObjectExporter().parse(s)
1677
1678    def x_testGetPageMeasureNumbers(self):
1679        from music21 import corpus
1680        c = corpus.parse('luca/gloria').parts[0]
1681        # c.show('text')
1682        retStr = ''
1683        for x in c.flatten():
1684            if 'PageLayout' in x.classes:
1685                retStr += str(x.pageNumber) + ': ' + str(x.measureNumber) + ', '
1686#        print(retStr)
1687        self.assertEqual(retStr, '1: 1, 2: 23, 3: 50, 4: 80, 5: 103, ')
1688
1689    def testGetStaffLayoutFromStaff(self):
1690        '''
1691        we have had problems with attributes disappearing.
1692        '''
1693        from music21 import corpus
1694        lt = corpus.parse('demos/layoutTest.xml')
1695        ls = divideByPages(lt, fastMeasures=True)
1696
1697        hiddenStaff = ls.pages[0].systems[3].staves[1]
1698        self.assertTrue(repr(hiddenStaff).endswith('Staff 11: p.1, sys.4, st.2>'),
1699                        repr(hiddenStaff))
1700        self.assertIsNotNone(hiddenStaff.staffLayout)
1701
1702
1703# ------------------------------------------------------------------------------
1704if __name__ == '__main__':
1705    import music21
1706    music21.mainTest(Test)  # , runTest='getStaffLayoutFromStaff')
1707
1708
1709