Source: music21/base.js

/**
 * music21j -- Javascript reimplementation of Core music21p features.
 * music21/base -- objects in base in music21p routines
 *
 * does not load the other modules.
 *
 * Copyright (c) 2013-19, Michael Scott Cuthbert and cuthbertLab
 * Based on music21 (=music21p), Copyright (c) 2006–19, Michael Scott Cuthbert and cuthbertLab
 *
 * module for Music21Objects, see {@link music21.base}
 *
 * @requires music21/common
 * @requires music21/duration
 * @requires music21/prebase
 * @requires music21/sites
 * @exports music21/base
 * @namespace music21.base
 * @memberof music21
 */
import * as common from './common.js';
import * as derivation from './derivation.js';
import * as duration from './duration.js';
import * as prebase from './prebase.js';
import * as sites from './sites.js';


/**
 * Base class for any object that can be placed in a {@link music21.stream.Stream}.
 *
 * @class Music21Object
 * @memberof music21.base
 * @extends music21.prebase.ProtoM21Object
 * @property {music21.stream.Stream} [activeSite] - hardlink to a {@link music21.stream.Stream} containing the element.
 * @property {number} classSortOrder - Default sort order for this class (default 20; override in other classes). Lower numbered objects will sort before other objects in the staff if priority and offset are the same.
 * @property {music21.duration.Duration} duration - the duration (object) for the element. (can be set with a quarterLength also)
 * @property {string[]} groups - An Array of strings representing group (equivalent to css classes) to assign to the object. (default [])
 * @property {boolean} isMusic21Object - true
 * @property {boolean} isStream - false
 * @property {number} offset - offset from the beginning of the stream (in quarterLength)
 * @property {number} priority - The priority (lower = earlier or more left) for elements at the same offset. (default 0)
 */
export class Music21Object extends prebase.ProtoM21Object {
    constructor(keywords) {
        super(keywords);
        this.classSortOrder = 20; // default;

        this._activeSite = undefined;
        this._naiveOffset = 0;

        // this._derivation = undefined;
        // this._style = undefined;
        // this._editorial = undefined;

        this._duration = new duration.Duration();
        /**
         *
         * @type {music21.derivation.Derivation|undefined}
         * @private
         */
        this._derivation = undefined; // avoid making extra objects...

        this._priority = 0; // default;

        this.id = sites.getId(this);
        this.groups = [];
        // groups
        this.sites = new sites.Sites();

        this.isMusic21Object = true;
        this.isStream = false;

        this.groups = []; // custom object in m21p

        // beat, measureNumber, etc.
        // lots to do...
        // noinspection JSUnusedLocalSymbols
        this._cloneCallbacks._activeSite = function Music21Object_cloneCallbacks_activeSite(
            keyName,
            newObj,
            self
        ) {
            newObj[keyName] = undefined;
        };
        this._cloneCallbacks._derivation = function Music21Music21Object_cloneCallbacks_derivation(
            keyName,
            newObj,
            self
        ) {
            const newDerivation = new derivation.Derivation(newObj);
            newDerivation.origin = self;
            newDerivation.method = 'clone';
            newObj[keyName] = newDerivation;
        };

        // noinspection JSUnusedLocalSymbols
        this._cloneCallbacks.sites = function Music21Object_cloneCallbacks_sites(
            keyName,
            newObj,
            self
        ) {
            newObj[keyName] = new sites.Sites();
        };
    }

    stringInfo() {
        let id16 = this.id;
        if (typeof id16 === 'number') {
            /**
             * @type {number}
             */
            const idNumber = id16;
            id16 = idNumber.toString(16);
            while (id16.length < 4) {
                id16 = '0' + id16;
            }
            id16 = '0x' + id16;
        }
        return id16;
    }

    get activeSite() {
        return this._activeSite;
    }

    set activeSite(site) {
        if (site === undefined) {
            this._activeSite = undefined;
            this._activeSiteStoredOffset = undefined;
        } else {
            try {
                site.elementOffset(this);
            } catch (e) {
                throw new sites.SitesException(
                    'activeSite cannot be set for an object not in the stream'
                );
            }
            this._activeSite = site;
        }
    }

    get derivation() {
        if (this._derivation === undefined) {
            this._derivation = new derivation.Derivation(this);
        }
        return this._derivation;
    }

    set derivation(newDerivation) {
        this._derivation = newDerivation;
    }


    get measureNumber() {
        if (this.activeSite !== undefined && this.activeSite.classes.includes('Measure')) {
            return this.activeSite.number;
        } else {
            const m = this.sites.getObjByClass('Measure');
            if (m !== undefined) {
                return m.number;
            } else {
                return undefined;
            }
        }
    }

    get offset() {
        if (this.activeSite === undefined) {
            return this._naiveOffset;
        } else {
            return this.activeSite.elementOffset(this);
        }
    }

    set offset(newOffset) {
        if (this.activeSite === undefined) {
            this._naiveOffset = newOffset;
        } else {
            this.activeSite.setElementOffset(this, newOffset);
        }
    }

    get priority() {
        return this._priority;
    }

    set priority(p) {
        this._priority = p;
    }

    /**
     * @type {music21.duration.Duration}
     */
    get duration() {
        return this._duration;
    }

    set duration(newDuration) {
        if (typeof newDuration === 'object') {
            this._duration = newDuration;
            // common errors below...
        } else if (typeof newDuration === 'number') {
            this._duration.quarterLength = newDuration;
        } else if (typeof newDuration === 'string') {
            this._duration.type = newDuration;
        }
    }

    /**
     * @type {number}
     */
    get quarterLength() {
        return this.duration.quarterLength;
    }

    set quarterLength(ql) {
        this.duration.quarterLength = ql;
    }

    /**
     *
     * @param {this} other
     * @returns {this}
     */
    mergeAttributes(other) {
        // id;
        this.groups = other.groups.slice();
        return this;
    }

    /**
     * Return the offset of this element in a given site -- use .offset if you are sure that
     * site === activeSite.
     *
     * Raises an Error if not in site.
     *
     * Does not change activeSite or .offset
     *
     * @param {music21.stream.Stream} site
     * @param {boolean} [stringReturns=false] -- allow strings to be returned
     * @returns {number|undefined}
     */
    getOffsetBySite(site, stringReturns=false) {
        if (site === undefined) {
            return this._naiveOffset;
        }
        return site.elementOffset(this, stringReturns);
    }

    /**
     * setOffsetBySite - sets the offset for a given Stream
     *
     * @param {music21.stream.Stream} site Stream object
     * @param {number} value offset
     */
    setOffsetBySite(site, value) {
        if (site !== undefined) {
            site.setElementOffset(this, value);
        } else {
            this._naiveOffset = value;
        }
    }


    /**
     * For an element which may not be in site, but might be in a Stream
     * in site (or further in streams), find the cumulative offset of the
     * clement in that site.
     *
     * See also music21.stream.iterator.RecursiveIterator.currentHierarchyOffset for
     * a method that is about 10x faster when running through a recursed stream.
     *
     * @param {music21.stream.Stream} site
     * @returns {Number|undefined}
     */
    getOffsetInHierarchy(site) {
        try {
            return this.getOffsetBySite(site);
        } catch (e) {} // eslint-disable-line no-empty
        // noinspection JSUnusedLocalSymbols
        for (const [csSite, csOffset, unused_csRecursionType] of this.contextSites()) {
            if (csSite === site) {
                return csOffset;
            }
        }
        throw new Error(`Element ${this} is not in hierarchy of ${site}`);
    }

    // ---------- Contexts -------------

    getContextByClass(className, options) {
        const payloadExtractor = function payloadExtractor(
            useSite,
            flatten,
            positionStart,
            getElementMethod,
            classList
        ) {
            // this should all be done as a tree...
            // do not use .flat or .semiFlat so as not
            // to create new sites.

            // VERY HACKY...
            let lastElement;
            for (let i = 0; i < useSite.length; i++) {
                const thisElement = useSite._elements[i];
                const indexOffset = useSite.elementOffset(thisElement);
                const matchClass = thisElement.isClassOrSubclass(classList);
                if (flatten === false && !matchClass) {
                    continue;
                } else if (!thisElement.isStream && !matchClass) {
                    continue;
                }
                // is a stream or an element of the appropriate class...
                // first check normal elements
                if (
                    getElementMethod.includes('Before')
                    && indexOffset >= positionStart
                ) {
                    if (
                        getElementMethod.includes('At')
                        && lastElement === undefined
                    ) {
                        lastElement = thisElement;
                        try {
                            lastElement.activeSite = useSite;
                        } catch (e) {
                            // do nothing... should not happen.
                        }
                    } else if (lastElement !== undefined
                                && lastElement.isClassOrSubclass(classList)) {
                        return lastElement;
                    } else if (matchClass) {
                        return thisElement;
                    }
                } else {
                    lastElement = thisElement;
                }
                if (
                    getElementMethod.includes('After')
                    && indexOffset > positionStart
                    && matchClass
                ) {
                    return thisElement;
                }
                // now check stream... already filtered out flatten == false;
                if (thisElement.isStream) {
                    const potentialElement = payloadExtractor(
                        thisElement,
                        flatten,
                        positionStart + indexOffset,
                        getElementMethod,
                        classList
                    );
                    if (potentialElement !== undefined) {
                        return potentialElement;
                    }
                }
            }
            if (lastElement !== undefined && lastElement.isClassOrSubclass(classList)) {
                return lastElement;
            } else {
                return undefined;
            }
        };

        const params = {
            getElementMethod: 'getElementAtOrBefore',
            sortByCreationTime: false,
        };
        common.merge(params, options);

        const getElementMethod = params.getElementMethod;
        const sortByCreationTime = params.sortByCreationTime;

        if (className !== undefined && !(className instanceof Array)) {
            className = [className];
        }
        if (
            getElementMethod.includes('At')
            && this.isClassOrSubclass(className)
        ) {
            return this;
        }

        for (const [site, positionStart, searchType] of this.contextSites({
            returnSortTuples: true,
            sortByCreationTime,
        })) {
            if (
                getElementMethod.includes('At')
                && site.isClassOrSubclass(className)
            ) {
                return site;
            }

            if (
                searchType === 'elementsOnly'
                || searchType === 'elementsFirst'
            ) {
                const contextEl = payloadExtractor(
                    site,
                    false,
                    positionStart,
                    getElementMethod,
                    className
                );
                if (contextEl !== undefined) {
                    try {
                        contextEl.activeSite = site;
                    } catch (e) {
                        // do nothing.
                    }
                    return contextEl;
                }
            } else if (searchType !== 'elementsOnly') {
                if (
                    getElementMethod.includes('After')
                    && (className === undefined
                        || site.isClassOrSubclass(className))
                ) {
                    if (
                        !getElementMethod.includes('NotSelf')
                        && this !== site
                    ) {
                        return site;
                    }
                }
                const contextEl = payloadExtractor(
                    site,
                    'semiFlat',
                    positionStart,
                    getElementMethod,
                    className
                );
                if (contextEl !== undefined) {
                    try {
                        contextEl.activeSite = site;
                    } catch (e) {
                        // do nothing.
                    }
                    return contextEl;
                }
                if (
                    getElementMethod.includes('Before')
                    && (className === undefined
                        || site.isClassOrSubclass(className))
                ) {
                    if (
                        !getElementMethod.includes('NotSelf')
                        || this !== site
                    ) {
                        return site;
                    }
                }
            }
        }

        return undefined;
    }

    * contextSites(options) {
        const params = {
            callerFirst: undefined,
            memo: [],
            offsetAppend: 0.0,
            sortByCreationTime: false,
            priorityTarget: undefined,
            returnSortTuples: false,
            followDerivation: true,
        };
        common.merge(params, options);
        const memo = params.memo;
        if (params.callerFirst === undefined) {
            params.callerFirst = this;
            if (this.isStream && !(this in memo)) {
                const recursionType = this.recursionType;
                yield [this, 0.0, recursionType];
            }
            memo.push(this);
        }

        if (params.priorityTarget === undefined && !params.sortByCreationTime) {
            params.priorityTarget = this.activeSite;
        }
        const topLevel = this;
        for (const siteObj of this.sites.yieldSites(
            params.sortByCreationTime,
            params.priorityTarget,
            true // excludeNone
        )) {
            if (memo.includes(siteObj)) {
                continue;
            }
            if (siteObj.classes.includes('SpannerStorage')) {
                continue;
            }

            // let offset = this.getOffsetBySite(siteObj);
            // followDerivation;
            const offsetInStream = siteObj.elementOffset(this);
            const newOffset = offsetInStream + params.offsetAppend;
            const positionInStream = newOffset;
            const recursionType = siteObj.recursionType;
            yield [siteObj, positionInStream, recursionType];
            memo.push(siteObj);

            const newParams = {
                callerFirst: params.callerFirst,
                memo,
                offsetAppend: positionInStream, // .offset
                returnSortTuples: true, // always!
                sortByCreationTime: params.sortByCreationTime,
            };
            for (const [
                topLevelInner,
                inStreamPos,
                recurType
            ] of siteObj.contextSites(newParams)) {
                const inStreamOffset = inStreamPos; // .offset;
                // const hypotheticalPosition = inStreamOffset; // more complex w/ sortTuples

                if (!memo.includes(topLevelInner)) {
                    // if returnSortTuples...
                    // else
                    yield [topLevelInner, inStreamOffset, recurType];
                    memo.push(topLevelInner);
                }
            }
        }
        // if followDerivation...
        if (params.followDerivation) {
            for (const derivedObject of topLevel.derivation.chain()) {
                for (const [derivedSite, derivedOffset, derivedRecurseType] of derivedObject.contextSites({
                    callerFirst: undefined,
                    memo,
                    offsetAppend: 0.0,
                    returnSortTuples: true,
                    sortByCreationTime: params.sortByCreationTime,
                })) {
                    const offsetAdjustedCsTuple = [derivedSite, derivedOffset + params.offsetAppend, derivedRecurseType];
                    yield offsetAdjustedCsTuple;
                }
            }
        }
    }
}

// TODO(msc) -- ElementWrapper

Music21j, Copyright © 2013-2021 Michael Scott Asato Cuthbert.
Documentation generated by JSDoc 3.6.3 on Wed Jul 31st 2019 using the DocStrap template.