All Downloads are FREE. Search and download functionalities are using the official Maven repository.

rapture.dp.WorkflowLayoutUtil Maven / Gradle / Ivy

There is a newer version: 3.0.4
Show newest version
/**
 * The MIT License (MIT)
 *
 * Copyright (c) 2011-2016 Incapture Technologies LLC
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */
package rapture.dp;

import java.awt.Point;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;

import org.apache.commons.lang.StringUtils;
import org.apache.log4j.Logger;

import rapture.common.dp.Step;
import rapture.common.dp.Transition;
import rapture.common.dp.Workflow;
import rapture.common.dp.WorkflowArrowLayout;
import rapture.common.dp.WorkflowBoxLayout;
import rapture.common.dp.WorkflowColumnLayout;
import rapture.common.dp.WorkflowGridLayout;
import rapture.dp.WorkflowLayoutUtil.BoxNote.Kind;

import com.google.common.collect.Lists;
import com.google.common.collect.Maps;

/**
 * Utility to create a customizable presentation of a workflow definition.
 * 
 * @author mel
 */
public class WorkflowLayoutUtil {
    private static final Logger log = Logger.getLogger(WorkflowLayoutUtil.class);

    public static WorkflowGridLayout makeGridLayout(Workflow workflow) {
        if (log.isTraceEnabled()) log.trace("Making layout from scratch for " + workflow.getWorkflowURI());
        return new LayoutMaker(workflow).make();
    }

    private static class LayoutMaker {
        final WorkflowGridLayout result = new WorkflowGridLayout();
        final Map name2note = Maps.newHashMap();
        final List boxes = Lists.newArrayList();
        final List panels = Lists.newArrayList();
        final Workflow workflow;
        final Map num2col = Maps.newHashMap();

        int idCount = 0;

        private LayoutMaker(Workflow workflow) {
            this.workflow = workflow;
        }

        WorkflowGridLayout make() {
            result.setArrows(new ArrayList());
            result.setColumns(new ArrayList());
            for (Step step : workflow.getSteps()) {
                BoxNote note = new BoxNote(step);
                name2note.put(step.getName(), note);
                boxes.add(note);
            }
            BoxNote startBox = new BoxNote(Kind.START);
            Panel startPanel = new Panel(startBox, null);
            BoxNote startStep = name2note.get(workflow.getStartStep());
            if (startStep != null) mapIsland(startBox, startStep, startPanel);
            panels.add(startPanel);
            for (BoxNote note : boxes) {
                if (note.panel == null) {
                    Panel panel = new Panel(note, null);
                    mapIsland(note, null, panel);
                    panels.add(panel);
                }
            }
            // now map the panels onto the grid.
            int left = 0;
            for (Panel panel : panels) {
                int min = panel.minX();
                int max = panel.maxX();
                int offset = left - min;
                left += min - max;
                for (BoxNote note : panel.boxNotes) {
                    note.gx = note.pgx + offset;
                    note.gy = note.pgy;
                }
            }
            // now make a layout from the notes
            for (BoxNote note : boxes) {
                WorkflowBoxLayout box = note.box;
                if (box == null) box = makeWorkflowBoxLayout(note);
                getOrMakeColumn(note.gx).getBoxes().add(box);
                for (ArrowNote arrowNote : note.inArrows) {
                    WorkflowArrowLayout arrow = new WorkflowArrowLayout();
                    arrow.setFromBoxName(getOrMakeBoxId(arrowNote.source));
                    arrow.setName(n2e(arrowNote.t.getName()));
                    result.getArrows().add(arrow);
                }
            }

            // Sort the Columns and add them to the result
            List columnKeys = Lists.newArrayList(num2col.keySet());
            Collections.sort(columnKeys);
            for (Integer key : columnKeys) {
                result.getColumns().add(num2col.get(key));
            }
            result.setWorkflowURI(workflow.getWorkflowURI());
            return result;
        }

        private String n2e(String in) {
            return (in == null) ? "" : in;
        }

        private String getOrMakeBoxId(BoxNote note) {
            if (note.box == null) makeWorkflowBoxLayout(note);
            return note.box.getId();
        }

        private WorkflowColumnLayout getOrMakeColumn(int col) {
            WorkflowColumnLayout column = num2col.get(col);
            if (column == null) {
                column = new WorkflowColumnLayout();
                column.setColumnNumber(col);
                num2col.put(col, column);
            }
            return column;
        }

        private WorkflowBoxLayout makeWorkflowBoxLayout(BoxNote note) {
            WorkflowBoxLayout result = new WorkflowBoxLayout();
            result.setGx(note.gx);
            result.setGy(note.gy);
            if (note.kind == Kind.STEP) {
                result.setName(note.step.getName());
            } else {
                result.setName("");
            }
            result.setKind(note.kind.toString());
            result.setId(nextId());
            note.box = result;
            return result;
        }

        private String nextId() {
            idCount++;
            return "_" + idCount;
        }

        private void mapIsland(BoxNote next, BoxNote previous, Panel panel) {
            if (previous == null) {
                next.pgx = 0;
                next.pgy = 0;
            } else {
                panel.placeNear(next, previous);
            }
            mapForward(next, panel);
            mapReverse(next, panel);
        }

        private void mapForward(BoxNote from, Panel panel) {
            List nextGen = Lists.newArrayList();
            for (Transition t : from.step.getTransitions()) {
                if (StringUtils.isEmpty(t.getName())) handleTransition(t, from, panel, nextGen);
            }
            for (Transition t : from.step.getTransitions()) {
                if (!StringUtils.isEmpty(t.getName())) handleTransition(t, from, panel, nextGen);
            }
            for (BoxNote note : nextGen) {
                mapForward(note, panel);
            }
        }

        private void handleTransition(Transition t, BoxNote from, Panel panel, List future) {
            String toName = t.getTargetStep();
            BoxNote toNote = name2note.get(toName);
            if (toNote == null) {
                toNote = makeBoxNoteFromToName(toName, panel, from, future);
                name2note.put(toName, toNote);
            }
            toNote.addArrow(from, t);
            // protects against infinite loop -- don't place the same box twice
            if (toNote.panel == null) {
                panel.placeNear(toNote, from);
            }
        }

        private static final Transition JOIN_TRANSITION;
        static {
            JOIN_TRANSITION = new Transition();
            JOIN_TRANSITION.setName("");
            JOIN_TRANSITION.setTargetStep("$JOIN");
        }

        private BoxNote makeBoxNoteFromToName(String toName, Panel parent, BoxNote fromNote, List future) {
            BoxNote result = new BoxNote();
            if (toName.startsWith("$")) {
                // handle special forms in transition
                if (toName.startsWith("$RETURN")) {
                    result.kind = Kind.RETURN;
                    if (toName.startsWith("$RETURN:")) {
                        result.decoration = toName.substring("$RETURN:".length());
                    }
                } else if (toName.startsWith("$JOIN")) {
                    result.kind = Kind.JOIN;
                    parent.tails.add(result);
                }
            } else {
                BoxNote note = name2note.get(toName);
                if (note == null) {
                    result.decoration = toName;
                    result.kind = Kind.UNDEFINED;
                    return result;
                } else {
                    if (note.step != null) {
                        if (note.step.getExecutable().startsWith("$SPLIT:")) {
                            if (note.panel != null) {
                                return note;
                            }
                            result.kind = Kind.SPLIT;
                            String forkNames[] = toName.substring("$SPLIT:".length()).split(",");
                            List forkPanels = Lists.newArrayList();
                            for (String forkName : forkNames) {
                                Panel p = makePanelForCall(forkName, parent);
                                forkPanels.add(p);
                            }
                            int width = 0;
                            int height = 1;
                            for (Panel p : forkPanels) {
                                width += p.getWidth();
                                int pHeight = p.getHeight();
                                if (pHeight > height) height = pHeight;
                            }
                            result.gw = width > 0 ? width : 1;
                            Point pos = parent.findVacantBlock(width, height + 2, fromNote);
                            parent.placeAt(pos.x, pos.y, result, true);
                            BoxNote footer = new BoxNote(Kind.JOIN);
                            footer.step = note.step; // header and footer use the same step
                            parent.placeAt(pos.x, pos.y + height + 1, footer, isTail(footer));
                            int x = pos.x;
                            for (Panel p : forkPanels) {
                                parent.transfer(x, pos.y + 1, p);
                                for (BoxNote tail : p.tails) {
                                    footer.addArrow(tail, JOIN_TRANSITION);
                                }
                            }
                            // extend from the footer, not the header
                            future.add(footer);
                            return note;
                        } else if (note.step.getExecutable().startsWith("$FORK:")) {
                            // TODO handle fork
                        } else if (note.step.getExecutable().startsWith("$FAIL")) {
                            note.kind = Kind.ERROR;
                            return note;
                        } else if (note.step.getExecutable().startsWith("$RETURN")) {
                            note.kind = Kind.RETURN;
                            if (note.step.getExecutable().startsWith("$RETURN:")) {
                                note.decoration = note.step.getExecutable().substring("$RETURN:".length());
                            }
                            return note;
                        }
                    }
                    future.add(note);
                    return note;
                }
            }
            return result;
        }

        private boolean isTail(BoxNote note) {
            List ts = note.step.getTransitions();
            if (ts == null) return false;
            for (Transition t : ts) {
                if (t.getTargetStep().startsWith("$JOIN")) return true;
            }
            return false;
        }

        private Panel makePanelForCall(String stepName, Panel parent) {
            BoxNote root = name2note.get(stepName);
            if (root == null) {
                BoxNote call = new BoxNote(Kind.CALL);
                Panel p = new Panel(call, parent);
                p.tails.add(call);
            }
            Panel result = new Panel(root, parent);
            mapIsland(root, null, result);
            return result;
        }

        private void mapReverse(BoxNote from, Panel panel) {
            // TODO glom connected islands together by backtracing transitions - may require prep phase
        }
    }

    static class BoxNote {
        public WorkflowBoxLayout box;

        BoxNote(Step step) {
            this.step = step;
            this.kind = Kind.STEP;
        }

        BoxNote(Kind k) {
            kind = k;
        }

        BoxNote() {
            this.kind = Kind.UNDEFINED;
        }

        enum Kind {
            STEP, START, RETURN, ERROR, UNDEFINED, SPLIT, JOIN, FORK, CALL
        };

        void addArrow(BoxNote from, Transition t) {
            inArrows.add(new ArrowNote(from, t));
        }

        Kind kind;
        Panel panel = null;
        Integer pgx = null; // grid position relative to panel
        Integer pgy = null;
        Integer gx = null; // master grid position
        Integer gy = null;
        int gw = 1; // width on the grid
        Step step;
        String decoration = null;
        List inArrows = Lists.newArrayList();
    }

    static class ArrowNote {
        final Transition t;
        final BoxNote source;

        ArrowNote(BoxNote from, Transition t) {
            this.t = t;
            this.source = from;
        }
    }

    static class Panel {
        final Panel parent;
        final List boxNotes = Lists.newArrayList();
        final List heads = Lists.newArrayList();
        final List tails = Lists.newArrayList();

        Panel(BoxNote anchor, Panel parent) {
            this.parent = parent;
            anchor.pgx = 0;
            anchor.pgy = 0;
            boxNotes.add(anchor);
            heads.add(anchor);
        }

        public void transfer(int x, int y, Panel p) {
            for (BoxNote note : boxNotes) {
                placeAt(x + note.pgx, y + note.pgy, note);
            }
        }

        public int minX() {
            int result = Integer.MAX_VALUE;
            for (BoxNote note : boxNotes) {
                if (note.pgx < result) result = note.pgx;
            }
            return result;
        }

        public int maxX() {
            int result = Integer.MIN_VALUE;
            for (BoxNote note : boxNotes) {
                if (note.pgx > result) result = note.pgx;
            }
            return result;
        }

        public int minY() {
            int result = Integer.MAX_VALUE;
            for (BoxNote note : boxNotes) {
                if (note.pgy < result) result = note.pgy;
            }
            return result;
        }

        public int maxY() {
            int result = Integer.MIN_VALUE;
            for (BoxNote note : boxNotes) {
                if (note.pgy > result) result = note.pgy;
            }
            return result;
        }

        public int getWidth() {
            return maxX() - minX() + 1;
        }

        public int getHeight() {
            return maxY() - minY() + 1;
        }

        // TODO hash this for performance x+":"+y as key -- wide elements need multiple keys
        public boolean isVacant(int x, int y) {
            for (BoxNote note : boxNotes) {
                if (note.pgx >= x && note.pgx + note.gw <= x && note.pgy == y) return false;
            }
            return true;
        }

        boolean placeAt(int x, int y, BoxNote note) {
            return placeAt(x, y, note, true);
        }

        /**
         * @returns false if already occupied
         */
        boolean placeAt(int x, int y, BoxNote note, boolean isTail) {
            if (isVacant(x, y)) {
                note.pgx = x;
                note.pgy = y;
                note.panel = this;
                if (isTail) tails.add(note);
                return true;
            }
            return false;
        }

        void placeNear(BoxNote note, BoxNote near) {
            int fx = near.pgx;
            int fy = near.pgy;
            if (!placeAt(fx, fy + 1, note)) if (!placeAt(fx + 1, fy, note)) if (!placeAt(fx - 1, fy, note)) {
                if (!placeAt(fx - 1, fy + 1, note)) if (!placeAt(fx + 1, fy + 1, note)) {
                    // TODO ESCALATE -- HARD TO PLACE
                }
            }
        }

        // TODO replace this brute force check with something vaguely performant
        Point findVacantBlock(int width, int height, BoxNote near) {
            int sx = near.gx;
            int sy = near.gy + 1;
            if (isVacantBlock(sx, sy, width, height)) return new Point(sx, sy);

            int roam = 1;
            while (true) {
                if (isVacantBlock(sx + roam, sy, width, height)) return new Point(sx, sy);
                if (isVacantBlock(sx, sy + roam, width, height)) return new Point(sx, sy);
                if (isVacantBlock(sx - roam, sy, width, height)) return new Point(sx, sy);
                roam++;
            }
        }

        boolean isVacantBlock(int x, int y, int w, int h) {
            for (int i = 0; i < w; i++) {
                for (int j = 0; j < h; j++) {
                    if (!isVacant(x + i, y + j)) return false;
                }
            }
            return true;
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy