com.cdd.bao.template.RenderSchema.java Source code

Java tutorial

Introduction

Here is the source code for com.cdd.bao.template.RenderSchema.java

Source

/*
 * BioAssay Ontology Annotator Tools
 * 
 * (c) 2014-2016 Collaborative Drug Discovery Inc.
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License 2.0
 * as published by the Free Software Foundation:
 * 
 * http://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html
 * 
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 */

package com.cdd.bao.template;

import com.cdd.bao.*;
import com.cdd.bao.template.*;
import com.cdd.bao.util.*;
import static com.cdd.bao.template.RenderContext.*;

import java.io.*;
import java.util.*;

import org.apache.pdfbox.pdmodel.*;
import org.apache.pdfbox.pdmodel.font.*;
import org.apache.pdfbox.pdmodel.common.*;

/*
   Rendering a schema as a publication quality PDF file.
*/

public class RenderSchema {
    private Schema schema;
    private PDDocument document = new PDDocument();
    private Vocabulary vocab;

    // context for rendering a page: the general idea is to make it a 2-pass system; for the first pass, the PDPage instance is null, and all operations
    // have no effects, except to update the maximum width/height; for the second pass
    private static final class Context {
        PDPage page = null;
        float x = 0, y = 0, w = 0, h = 0;
    }

    // ------------ public methods ------------   

    public RenderSchema(Schema schema) {
        this.schema = schema;
        vocab = Vocabulary.globalInstance();
    }

    // creates a new page and crams the template onto it
    public void createPageTemplate() throws IOException {
        RenderContext ctx = new RenderContext();

        renderPageTemplate(ctx);
        if (ctx.width == 0 || ctx.height == 0)
            return;

        PDPage page = new PDPage(new PDRectangle(ctx.width, ctx.height));
        document.addPage(page);

        ctx.stream = new PDPageContentStream(document, page);
        renderPageTemplate(ctx);
        ctx.stream.close();
    }

    // creates a page showing the given assay and its annotations
    public void createPageAssay(Schema.Assay assay) throws IOException {
        RenderContext ctx = new RenderContext();

        renderPageTemplate(ctx);
        if (ctx.width == 0 || ctx.height == 0)
            return;

        PDPage page = new PDPage(new PDRectangle(ctx.width, ctx.height));
        document.addPage(page);

        ctx.stream = new PDPageContentStream(document, page);
        renderPageAssay(ctx, assay);
        ctx.stream.close();
    }

    // creates a page showing the property & value hierarchies, respectively   
    public void createPageProperties() throws IOException {
        RenderContext ctx = new RenderContext();

        renderPageHierarchy(ctx, vocab.getPropertyHierarchy());
        if (ctx.width == 0 || ctx.height == 0)
            return;

        PDPage page = new PDPage(new PDRectangle(ctx.width, ctx.height));
        document.addPage(page);

        ctx.stream = new PDPageContentStream(document, page);
        renderPageHierarchy(ctx, vocab.getPropertyHierarchy());
        ctx.stream.close();
    }

    public void createPageValues() throws IOException {
        RenderContext ctx = new RenderContext();

        renderPageHierarchy(ctx, vocab.getValueHierarchy());
        if (ctx.width == 0 || ctx.height == 0)
            return;

        PDPage page = new PDPage(new PDRectangle(ctx.width, ctx.height));
        document.addPage(page);

        ctx.stream = new PDPageContentStream(document, page);
        renderPageHierarchy(ctx, vocab.getValueHierarchy());
        ctx.stream.close();
    }

    public void write(File file) throws IOException {
        document.save(file);
        //document.close();
    }

    // ------------ private methods ------------   

    private void renderPageTemplate(RenderContext ctx) {
        float y = 0;
        final float pad = 5;

        Schema.Group root = schema.getRoot();
        float nameW = ctx.measureLine(root.name, 15)[0];
        ctx.drawLine(0, 1, nameW, 1, 0x808080, 0.5f);
        ctx.drawText(0, y, root.name, 15, 0x000000, TXTALIGN_LEFT | TXTALIGN_TOP);
        y += 20;
        ctx.drawText(0, y, "URI: " + schema.getSchemaPrefix(), 8, 0x808080, TXTALIGN_LEFT | TXTALIGN_TOP);
        y += 15;
        // !! .descr, as a paragraph
        ctx.drawLine(0, y, nameW, y, 0x808080, 0.5f);

        y += pad;

        // create a flattened assignment list
        List<Schema.Assignment> assignments = new ArrayList<>();
        List<Schema.Group> stack = new ArrayList<>();
        stack.add(root);
        while (stack.size() > 0) {
            Schema.Group grp = stack.remove(0);

            // add each assignment with a sorted & truncated list of values
            //assignments.addAll(grp.assignments);
            final int MAX_VALUES = 10;
            for (Schema.Assignment assn : grp.assignments) {
                assn = assn.clone();
                assn.values.sort((v1, v2) -> v1.name.compareToIgnoreCase(v2.name));
                if (assn.values.size() > MAX_VALUES) {
                    int nkeep = MAX_VALUES - 1, nextra = assn.values.size() - nkeep;
                    while (assn.values.size() > nkeep)
                        assn.values.remove(nkeep);
                    assn.values.add(new Schema.Value("", "(+ " + nextra + " more)"));
                }
                assignments.add(assn);
            }

            stack.addAll(grp.subGroups);
        }

        // estimate widths & heights for each assignment, then split into blocks of columns
        float[] assnW = new float[assignments.size()], assnH = new float[assignments.size()];
        for (int n = 0; n < assignments.size(); n++) {
            Schema.Assignment assn = assignments.get(n);
            assnW[n] = 0;
            for (Schema.Value val : assn.values)
                assnW[n] = Math.max(assnW[n], ctx.measureLine(val.name, 10)[0]);
            String propLabel = vocab.getLabel(assn.propURI);
            float titleW = Math.max(ctx.measureLine(assn.name, 12)[0], ctx.measureLine(propLabel, 8)[0] + 5);
            assnW[n] += titleW + 50 + 5 * pad;
            assnH[n] = Math.max(27, assn.values.size() * 12);
            if (assn.parent.parent != null)
                assnH[n] += 15;
        }
        int[][] blocks = arrangeColumns(assnW, assnH);

        float topY = y, leftX = 0;

        for (int[] blk : blocks) {
            float curY = topY, maxX = leftX;
            for (int b : blk) {
                float[] xy = renderAssignment(ctx, leftX, curY, assignments.get(b));
                maxX = Math.max(maxX, xy[0]);
                curY = xy[1] + pad;
            }
            leftX = maxX;
        }
    }

    private void renderPageAssay(RenderContext ctx, Schema.Assay assay) {
        float y = 0;
        final float pad = 5;

        float nameW = Math.max(ctx.measureLine(assay.name, 15)[0], ctx.measureLine(assay.originURI, 8)[0]);
        ctx.drawLine(0, 1, nameW, 1, 0x808080, 0.5f);
        ctx.drawText(0, y, assay.name, 15, 0x000000, TXTALIGN_LEFT | TXTALIGN_TOP);
        y += 20;
        if (assay.originURI.length() > 0) {
            ctx.drawText(0, y, "Origin: " + assay.originURI, 8, 0x808080, TXTALIGN_LEFT | TXTALIGN_TOP);
            y += 15;
        }
        ctx.drawLine(0, y, nameW, y, 0x808080, 0.5f);

        y += pad;

        List<Schema.Annotation> pool = new ArrayList<>(assay.annotations);
        List<Schema.Group> groupList = new ArrayList<>();
        groupList.add(schema.getRoot());

        // recursively add all known assignments, in depth order of groups
        while (groupList.size() > 0) {
            Schema.Group group = groupList.remove(0);

            for (Schema.Assignment assn : group.assignments) {
                // insert any annotations that match this assignment; more than one is a possibility that will be reflected (though not necessarily valid); if none
                // were found, manufacture the unassigned state
                List<Schema.Annotation> matches = new ArrayList<>();
                for (int n = 0; n < pool.size(); n++) {
                    Schema.Annotation annot = pool.get(n);
                    if (schema.matchAnnotation(annot, assn)) {
                        matches.add(annot);
                        pool.remove(n);
                        n--;
                    }
                }

                float[] xy = renderAnnotation(ctx, 0, y, assn,
                        matches.toArray(new Schema.Annotation[matches.size()]));
                y = xy[1];
            }
            for (int n = group.subGroups.size() - 1; n >= 0; n--)
                groupList.add(0, group.subGroups.get(n));
        }
    }

    private void renderPageHierarchy(RenderContext ctx, Vocabulary.Hierarchy hier) {
        // flatten out the branches, and keep them in order
        List<Vocabulary.Branch> segments = new ArrayList<>(hier.rootBranches);
        final int MAX_CHILDREN = 10;
        for (int n = 0; n < segments.size(); n++) {
            Vocabulary.Branch branch = segments.get(n);
            int pos = n + 1;
            for (Vocabulary.Branch child : branch.children)
                if (child.children.size() > 0)
                    segments.add(pos++, child.clone());
        }

        // sort & truncate as necessary
        for (Vocabulary.Branch branch : segments) {
            branch.children.sort((v1, v2) -> v1.label.compareToIgnoreCase(v2.label));
            if (branch.children.size() > MAX_CHILDREN) {
                int nkeep = MAX_CHILDREN - 1, nextra = branch.children.size() - nkeep;
                while (branch.children.size() > nkeep)
                    branch.children.remove(nkeep);
                branch.children.add(new Vocabulary.Branch("", "(+ " + nextra + " more)"));
            }
        }

        float y = 0;
        final float pad = 5;

        // estimate widths & heights for each assignment, then split into blocks of columns
        float[] segW = new float[segments.size()], segH = new float[segments.size()];
        for (int n = 0; n < segments.size(); n++) {
            Vocabulary.Branch branch = segments.get(n);
            segW[n] = 0;
            for (Vocabulary.Branch child : branch.children)
                segW[n] = Math.max(segW[n], ctx.measureLine(child.label, 10)[0]);
            segW[n] += ctx.measureLine(branch.label, 12)[0] + 50 + 5 * pad;
            segH[n] = Math.max(15, branch.children.size() * 12);
        }
        int[][] blocks = arrangeColumns(segW, segH);

        float topY = y, leftX = 0;

        for (int[] blk : blocks) {
            float curY = topY, maxX = leftX;
            for (int b : blk) {
                float[] xy = renderBranch(ctx, leftX, curY, segments.get(b));
                maxX = Math.max(maxX, xy[0]);
                curY = xy[1] + pad;
            }
            leftX = maxX;
        }
    }

    private float[] renderAssignment(RenderContext ctx, float x, float y, Schema.Assignment assn) {
        final float pad = 5;

        // draw the group label, if not directly descended from root
        Schema.Group group = assn.parent;
        float maxX = x;
        if (group.parent != null) {
            //String label = group.name;
            //for (Schema.Group look = group.parent; look.parent != null; look = look.parent) label = look.name + "\u25BA" + label;
            String label = "";
            for (Schema.Group look = group; look.parent != null; look = look.parent)
                label = look.name + " / " + label;
            ctx.drawText(x + pad, y, label, 12, 0x000000, TXTALIGN_LEFT | TXTALIGN_TOP);
            y += 15;
            maxX = x + pad + ctx.measureLine(label, 12)[0];
        }

        float ly = y, ry = y;
        String propLabel = vocab.getLabel(assn.propURI);

        float nameW = ctx.measureLine(assn.name, 12)[0];
        float labelW = Math.max(nameW, ctx.measureLine(propLabel, 8)[0] + pad);
        float arrowW = Math.max(20, labelW - nameW);
        ctx.drawText(x + pad, ly, assn.name, 12, 0x000000, TXTALIGN_LEFT | TXTALIGN_TOP);
        drawArrow(ctx, x + 2 * pad + nameW, ly + 9, arrowW, 5);
        ly += 15;

        ctx.drawText(x + 2 * pad, ly, propLabel, 8, 0x404040, TXTALIGN_LEFT | TXTALIGN_TOP);
        ly += 12;
        // !! .descr, as paragraph

        float rightX = x + 3 * pad + nameW + arrowW;

        for (Schema.Value val : assn.values) {
            String label = val.name;
            //label += " " + val.uri;
            ctx.drawText(rightX, ry, label, 10, 0x404040, TXTALIGN_LEFT | TXTALIGN_TOP);
            ry += 12;
            maxX = Math.max(maxX, rightX + ctx.measureLine(label, 10)[0]);
        }

        ctx.drawLine(rightX - 0.5f * pad, y, rightX - 0.5f * pad, ry, 0xC0C0C0, 1);

        return new float[] { maxX, Math.max(ly, ry) };
    }

    private float[] renderAnnotation(RenderContext ctx, float x, float y, Schema.Assignment assn,
            Schema.Annotation[] annots) {
        final float pad = 5;

        String assnName = assn.name;
        for (Schema.Group p = assn.parent; p.parent != null; p = p.parent)
            assnName = p.name + " \u25BA " + assnName;

        String propLabel = vocab.getLabel(assn.propURI);
        float nameW = ctx.measureLine(assnName, 12)[0], arrowW = 20, propW = ctx.measureLine(propLabel, 8)[0];
        arrowW += Math.max(0, 2 * (0.5f * propW - nameW - pad - 0.5f * arrowW));

        float cx = Math.max(0.5f * propW, nameW + pad + 0.5f * arrowW);

        ctx.drawText(cx, y, propLabel, 8, 0x808080, TXTALIGN_CENTRE | TXTALIGN_TOP);
        y += 6;

        //float nx = Math.max(0, 0.5f * propW - nameW);
        ctx.drawText(cx - 0.5f * arrowW - pad, y, assnName, 12, 0x000000, TXTALIGN_RIGHT | TXTALIGN_TOP);
        drawArrow(ctx, cx - 0.5f * arrowW, y + 9, arrowW, 5);

        float ax = cx + 0.5f * arrowW + pad;
        for (int n = 0; n < Math.max(1, annots.length); n++) {
            String label = "(not assigned)";
            int col = 0x808080;
            if (annots.length == 0) {
            } else if (annots[n].literal != null) {
                label = "\"" + annots[n].literal + "\"";
                col = 0x804000;
            } else if (annots[n].value != null) {
                label = annots[n].value.name;
                col = 0x000000;
            }
            ctx.drawText(ax, y, label, 12, col, TXTALIGN_LEFT | TXTALIGN_TOP);
            y += 12;
        }

        y += pad;

        return new float[] { x, y };
    }

    private float[] renderBranch(RenderContext ctx, float x, float y, Vocabulary.Branch branch) {
        final float pad = 5;

        float ly = y, ry = y;

        float nameW = ctx.measureLine(branch.label, 12)[0], arrowW = 50;
        ctx.drawText(x + pad, ly, branch.label, 12, 0x000000, TXTALIGN_LEFT | TXTALIGN_TOP);

        if (branch.children.size() == 0)
            return new float[] { x + 2 * pad + nameW, ly + 15 };

        drawArrow(ctx, x + 2 * pad + nameW, ly + 9, arrowW, 5);
        ly += 15;

        float rightX = x + 3 * pad + nameW + arrowW;

        float maxX = rightX;
        for (Vocabulary.Branch child : branch.children) {
            ctx.drawText(rightX, ry, child.label, 10, 0x404040, TXTALIGN_LEFT | TXTALIGN_TOP);
            ry += 12;
            maxX = Math.max(maxX, rightX + ctx.measureLine(child.label, 10)[0]);
        }

        ctx.drawLine(rightX - 0.5f * pad, y, rightX - 0.5f * pad, ry, 0xC0C0C0, 1);

        return new float[] { maxX, Math.max(ly, ry) };
    }

    private void drawArrow(RenderContext ctx, float x, float y, float w, float sz) {
        ctx.drawLine(x, y, x + w, y, 0x000000, 1);
        float[] px = new float[] { x + w, x + w - sz, x + w - sz };
        float[] py = new float[] { y, y - 0.5f * sz, y + 0.5f * sz };
        ctx.drawPoly(px, py, NOCOLOUR, 0, 0x000000, true);
    }

    // given a series of objects of a certain height, returns a collection of blocks that represent an attempt to arrange them with columns
    // of an even height; not an exact science, but it's better than random
    private int[][] arrangeColumns(float[] width, float[] height) {
        // pick a maximum height: columns will be stacked up to this size if possible
        final int num = width.length;
        float totalArea = 0, unitH = 0;
        for (int n = 0; n < num; n++) {
            totalArea += width[n] * height[n];
            unitH = Math.max(unitH, height[n]);
        }

        float maxH = Math.max(unitH, (float) Math.sqrt(totalArea));

        List<int[]> blocks = new ArrayList<>();
        for (int n = 0; n < num; n++)
            blocks.add(new int[] { n });

        while (true) {
            int best1 = -1, best2 = -1;
            float score = Float.NaN;

            for (int i = 0; i < blocks.size() - 1; i++)
                for (int j = i + 1; j < blocks.size(); j++) {
                    float residual = maxH;
                    for (int n : blocks.get(i))
                        residual -= height[n];
                    for (int n : blocks.get(j))
                        residual -= height[n];
                    if (residual < 0)
                        continue; // not allowed to go over
                    if (best1 < 0 || residual < score) {
                        best1 = i;
                        best2 = j;
                        score = residual;
                    }
                }

            if (best1 < 0)
                break;
            int[] blk1 = blocks.get(best1), blk2 = blocks.get(best2);
            int[] blk = Arrays.copyOf(blk1, blk1.length + blk2.length);
            for (int n = 0; n < blk2.length; n++)
                blk[blk1.length + n] = blk2[n];
            blocks.set(best1, blk);
            blocks.remove(best2);
        }

        for (int[] blk : blocks)
            Arrays.sort(blk);

        // put the smallest one last: there's usually an odd one out, and it looks better that way
        int smallest = 0;
        float minH = Float.POSITIVE_INFINITY;
        for (int n = 0; n < blocks.size(); n++) {
            float h = 0;
            for (int b : blocks.get(n))
                h += height[b];
            if (h < minH) {
                smallest = n;
                minH = h;
            }
        }
        blocks.add(blocks.remove(smallest));

        return blocks.toArray(new int[blocks.size()][]);
    }
}