FeatureContent.prototype.renderEntities = function()

in lib/@uncharted/strippets/src/strippets.outline.feature.js [185:409]


FeatureContent.prototype.renderEntities = function(entities) {
    var $entities;
    var entitiesRenderMap = [];

    // get the number of pixels we will be working with, which is the height of the container - room for the entity at the bottom.
    this.height = this.height || this.$outlineEntityContainer.height();
    var entityPercentageHeight = (this.entityHeight / this.height);
    var thresholdPercentageHeight = (this.Settings.entityLayoutThreshold / this.height);

    // Don't try to use fround when it doesn't exist (IE)
    if (Math.fround) {
        // fround it to avoid precision errors.
        entityPercentageHeight = Math.fround(entityPercentageHeight);
        thresholdPercentageHeight = Math.fround(thresholdPercentageHeight);
    }

    // order entities first by weight then by position (assume between 0-1)
    var orderedEntities = _.sortBy(entities || this.entities, function(entity) {
        return Number(entity.data.firstPosition) - entity.weight;
    });

    if (orderedEntities && orderedEntities.length > 0 && entityPercentageHeight > 0 &&
        (entityPercentageHeight !== this.entityPercentageHeight || !_.isEqual(this.entitiesShown, entities))) {
        this.entityPercentageHeight = entityPercentageHeight;
        this.entitiesShown = entities;

        // first pass: place the entities if possible
        orderedEntities.forEach(function(entity) {
            var entityGroup = _.find(entitiesRenderMap, function(map) {
                return (map.position.originalFrom <= Number(entity.data.firstPosition) &&
                    map.position.originalTo >= Number(entity.data.firstPosition)) ||
                    (map.position.originalFrom <= (Number(entity.data.firstPosition) + entityPercentageHeight) &&
                    map.position.originalTo >= (Number(entity.data.firstPosition) + entityPercentageHeight));
            });

            if (!entityGroup) {
                entityGroup = {
                    position: new EntityRenderMap(entity.data.firstPosition, entityPercentageHeight),
                    entityKeys: {},
                    entities: [],
                };
                entitiesRenderMap.push(entityGroup);
            }

            var key;
            if (entity.data.hasOwnProperty(consolidationField)) {
                key = entity.data[consolidationField];
            } else {
                key = Object.keys(entityGroup).toString();
            }

            if (!entityGroup.entityKeys[key]) {
                entityGroup.entityKeys[key] = [entityGroup.entities.length];
            } else {
                entityGroup.entityKeys[key].push(entityGroup.entities.length);
            }

            var entityMap = {
                position: new EntityRenderMap(entity.data.firstPosition, entityPercentageHeight),
                entity: entity,
                hiddenEntities: [],
                key: key,
            };

            entityGroup.entities.push(entityMap);
        });

        // second pass : try to place remaining entities if there is enough space. This can only happen if the configured threshold is greater than the size of entity.
        // first make sure that all entities are ordered in ascending position sequence (weighting could have shifted everything around previously).
        entitiesRenderMap = _.sortBy(entitiesRenderMap, function(map) {
            return map.position.originalFrom;
        });

        if (thresholdPercentageHeight >= this.entityPercentageHeight) {
            var index;
            var length = entitiesRenderMap.length;
            var list = entitiesRenderMap;
            entitiesRenderMap = [];

            for (index = 0; index < length; index++) {
                // check if there is enough space given the threshold
                var currentGroup = list[index];
                var groupKeys = Object.keys(currentGroup.entityKeys);

                // only reposition if the group has more than one entity
                if (groupKeys.length > 1) {
                    var entityToFit;
                    var entityIndex;
                    var currentEntity = currentGroup.entities[0];

                    // if we already spread some entities around, account for them
                    var beforeSpace;
                    if (index > 0) {
                        var previous = list[index - 1].entities[0];
                        var lastRepositioned = entitiesRenderMap[entitiesRenderMap.length - 1];
                        if (lastRepositioned && lastRepositioned.position.finalTo > previous.position.finalTo) {
                            previous = lastRepositioned;
                        }

                        beforeSpace = Math.min(currentEntity.position.originalFrom - previous.position.finalTo, thresholdPercentageHeight);
                    } else {
                        beforeSpace = Math.min(currentEntity.position.originalFrom, thresholdPercentageHeight);
                    }
                    var afterSpace = Math.min(index < list.length - 1 ? list[index + 1].position.originalFrom - currentEntity.position.originalTo : 1 - currentEntity.position.originalTo, thresholdPercentageHeight);

                    // get available space, which is the available space before + the available space after + the space the entity takes up.
                    var availableSpace = beforeSpace
                        + afterSpace
                        + currentEntity.position.originalTo - currentEntity.position.originalFrom;

                    // number of entities to fit is however many is allowed in the given space.
                    var entitiesToFitCount = Math.floor(availableSpace / entityPercentageHeight);
                    // only reposition if there is enough room for more than 1 entity.
                    if (entitiesToFitCount > 1) {
                        var neededSpace = entitiesToFitCount * entityPercentageHeight;
                        // starting position should be the (available space FROM) + ((Available Space - Needed Space) / 2)
                        var availableSpaceFrom = currentEntity.position.originalFrom - beforeSpace;
                        var startingFrom = availableSpaceFrom + ((availableSpace - neededSpace) / 2);

                        // determine entity positioning (weight and priority don't get taken into account here as it's just placement)
                        // find which entities should be placed separately and which ones should be merged to the top most entity
                        var processedKeys = {};
                        var entitiesToFit = [];
                        currentGroup.entities.forEach(distributeEntities.bind(null, processedKeys, entitiesToFit, entitiesToFitCount, currentGroup));

                        // update new position
                        for (entityIndex = 0; entityIndex < entitiesToFit.length; entityIndex++) {
                            entityToFit = entitiesToFit[entityIndex];
                            entityToFit.position.finalFrom = startingFrom + (entityIndex * entityPercentageHeight);
                            entitiesRenderMap.push(entityToFit);
                        }
                    } else {
                        entitiesRenderMap.push(flattenGroup(currentGroup));
                    }
                } else if (currentGroup.entities.length) {
                    entitiesRenderMap.push(flattenGroup(currentGroup));
                }
            }
        }

        $entities = entitiesRenderMap.map(function(map) {
            map.entity.setPosition(map.position.finalFrom * 100);
            if (map.hiddenEntities && map.hiddenEntities.length > 0) {
                if (map.hiddenEntities[0].entity.data.hasOwnProperty(consolidationField)) {
                    // For the Uncertainty feature,
                    // 1. consolidate adjacent entities of the same entity ID
                    var consolidatedEntities = [map].concat(map.hiddenEntities).sort(function (a, b) {
                        if (a.entity.data[consolidationField] < b.entity.data[consolidationField]) {
                            return -1;
                        }
                        if (a.entity.data[consolidationField] > b.entity.data[consolidationField]) {
                            return 1;
                        }
                        return compareEntities(a, b);
                    });
                    var consolidatedEntityCount = consolidatedEntities.length;
                    var types = [];
                    var uniqueEntities = [];
                    var i;
                    for (i = 0; i < consolidatedEntityCount; i++) {
                        if (i < 1 || consolidatedEntities[i].entity.data[consolidationField] !==
                            consolidatedEntities[i - 1].entity.data[consolidationField]) {
                            uniqueEntities.push(consolidatedEntities[i]);
                            pushTypes(uniqueEntities, 2, types);
                            types = [consolidatedEntities[i].entity.data];
                        } else {
                            types.push(consolidatedEntities[i].entity.data);
                        }
                    }

                    pushTypes(uniqueEntities, 1, types);

                    uniqueEntities.sort(compareEntities);

                    consolidatedEntityCount = uniqueEntities.length;

                    // 2. label the resulting entity with its types, in bucket order, e.g. Amazon [LOC, ORG], Samsung, ...
                    var appendTooltip = function (a, b) {
                        var result = a;
                        if (b.type !== uniqueEntities[i].entity.data.name) {
                            if (a) {
                                result += ', ';
                            }
                            result += b.type;
                        }
                        return result;
                    };

                    var tooltip = '';
                    for (i = 0; i < consolidatedEntityCount; i++) {
                        if (tooltip.length) {
                            tooltip += ', ';
                        }
                        tooltip += uniqueEntities[i].entity.data.name;

                        if (uniqueEntities[i].types && !identical(uniqueEntities[i].types, getEntityType)) {
                            tooltip += ' [' + uniqueEntities[i].types.reduce(appendTooltip, '') + ']';
                        }
                    }

                    map.entity.setAttributes({
                        'data-hidden-entities': consolidatedEntityCount > 1 ? consolidatedEntityCount : null,
                        'data-entities': tooltip,
                    });
                } else {
                    map.entity.setAttributes({
                        'data-hidden-entities': map.hiddenEntities.length,
                        'data-entities': [map].concat(map.hiddenEntities).sort(compareEntities).reduce(
                            function(memo, m) {
                                return (memo !== '' ? memo + ', ' : '') + m.entity.data.name;
                            }, ''),
                    });
                }
            } else {
                map.entity.setAttributes({
                    'data-hidden-entities': null,
                    'data-entities': map.entity.data.name,
                });
            }
            map.entity.highlight(map.entity.isHighlight, map.entity.color);
            return map.entity.$entity;
        });
        this.$outlineEntityContainer.html($entities);
    }
};