basic sample project

This commit is contained in:
Andy Clement
2016-05-19 09:06:50 -07:00
parent 135fd1c3e5
commit 321475500c
17 changed files with 1667 additions and 0 deletions

View File

@@ -0,0 +1,62 @@
# A sample app for spring-flo
This is a very basic example of spring-flo usage.
# Running the sample
A basic Spring Boot app is used to serve the sample. Launch it with:
mvn spring-boot:run
then open http://localhost:8080 then you can type text into the box
(e.g. "filewatch > ftp") or drag elements from the palette and link them
together to build a pipeline.
# Understanding the sample
Spring Flo allows simultaneous synchronization between some textual domain
specific language (DSL) and the graphical representation. You can type in the
text version or you can interactively build the graph version. Any custom
usage of spring-flo therefore needs three things:
- a metamodel of the domain. What are the 'things' you are connecting in your
pipeline and what are their properties?
- a converter from the text form to the graph form.
- a converter from the graph form to the text form.
The same here includes a very basic domain that you can easily customize:
- there are a handful of nodes with basic properties. These are defined in
`metamodel-sample.json` - that is a JSON array of the modules supported.
- the DSL form is `module --key=value --key=value | nextModule --key=value`.
- the conversion from text to graph is done in `text-to-graph.js`
- the conversion from graph to text is done in `graph-to-text.js`
Note the convertors (and the DSL) do not describe or support linking multiple nodes
to a single node, but spring-flo supports that, you just need to enhance the
convertors and have a representation in the DSL.
If you wanted to use this sample in your domain, perhaps you would enhance `index.html`
to include some kind of 'execute' button that used the text representation to
drive some other (perhaps backend) API.
The included `index.html` has basic buttons that exercise some of the control features
of flo - for example turning off the palette and switching it to a read-only mode (for
use when embedding flo into something that should just be showing a read only view of a
pipeline).
# Navigating the sample
In order to keep the project to a single build file, spring-flo is referenced
through the maven pom using webjars dependencies.
- `index.html` the single HTML file that embeds sping-flo
- `main.js` the launch config file for requirejs, referenced from index.html
- `sample-app.js` setup the angular app with custom services
- `metamodel-service.js` the custom metamodel service for this usage of spring-flo
- `editor-service.js` the custom editor service for this usage of spring-flo
- `render-service.js` the custom render service for this usage of spring-flo
- `metamodel-sample.json` a basic json description of the domain
- `graph-to-text.js` convert from the graph form to our custom DSL text
- `text-to-graph.js` convert from the custom DSL text to our graph form

115
samples/spring-flo-sample/pom.xml Executable file
View File

@@ -0,0 +1,115 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.springframework</groupId>
<artifactId>spring-flo-sample</artifactId>
<version>0.0.1.BUILD-SNAPSHOT</version>
<packaging>jar</packaging>
<name>spring-flo-sample</name>
<description>Spring Flo Sample</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.3.2.RELEASE</version>
<relativePath /> <!-- lookup parent from repository -->
</parent>
<!-- TODO maybe adjust flos dependencies so this configuration isn't so complicated -->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.webjars.bower</groupId>
<artifactId>codemirror</artifactId>
<version>5.1.0</version>
</dependency>
<dependency>
<groupId>org.webjars.bower</groupId>
<artifactId>angular</artifactId>
<version>1.3.8</version> <!-- flo wants 1.3.5 -->
</dependency>
<dependency>
<groupId>org.webjars.bower</groupId>
<artifactId>jshint</artifactId>
<version>2.8.0</version> <!-- flo wants 2.6.3 -->
</dependency>
<dependency>
<groupId>org.webjars.bower</groupId>
<artifactId>requirejs</artifactId>
<version>2.1.18</version> <!-- flo wants 2.1.15 -->
</dependency>
<dependency>
<groupId>org.webjars.bower</groupId>
<artifactId>jquery</artifactId>
<version>2.2.0</version> <!-- jointjs wants 2.0.3 -->
</dependency>
<dependency>
<groupId>org.webjars.bower</groupId>
<artifactId>lodash</artifactId>
<version>3.10.1</version> <!-- jointjs -> graphlib -> wants 3.10.1-amd -->
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.webjars</groupId>
<artifactId>webjars-locator</artifactId>
<version>0.31</version>
</dependency>
<dependency>
<groupId>org.webjars.bower</groupId>
<artifactId>requirejs-domready</artifactId>
<version>2.0.1</version>
</dependency>
<dependency>
<groupId>org.webjars.bower</groupId>
<artifactId>requirejs-text</artifactId>
<version>2.0.15</version>
</dependency>
<dependency>
<groupId>org.webjars.bower</groupId>
<artifactId>jointjs</artifactId>
<version>0.9.7</version> <!-- flo wants 0.9.6 -->
</dependency>
<dependency>
<groupId>org.webjars.bower</groupId>
<artifactId>json5</artifactId>
<version>0.4.0</version>
</dependency>
<dependency>
<groupId>org.webjars.bower</groupId>
<artifactId>spring-flo</artifactId>
<version>0.5.0</version>
<exclusions>
<exclusion>
<groupId>org.webjars.bower</groupId>
<artifactId>joint</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<start-class>org.springframework.flo.Application</start-class>
<java.version>1.7</java.version>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>

View File

@@ -0,0 +1,33 @@
/*
* Copyright 2016 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.flo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
*
* @author Andy Clement
* @author Alex Boyko
*/
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}

View File

@@ -0,0 +1,141 @@
.header {
font-weight: 400;
font-family: "Varela Round",sans-serif;
font-size: 36px;
color: #eeeeee;
padding: 2px;
background-color: #34302d;
border: none;
border-top: 4px solid #6db33f;
z-index: 1;
}
body {
background-color: #eeeeee;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-o-user-select: none;
user-select: none;
}
.control-button {
width: 16px;
height: 16px;
}
.header-small {
font: 300 24px "Helvetica Neue";
}
pre {
font-size: 18px;
}
.border-selected {
stroke: #34302d;
stroke-width: 3;
}
.controls {
border-radius: 2px;
border: solid;
border-color: #6db33f;
padding: 5px;
margin-top: 3px;
background-color: #eeeeee;
border-width: 1px;
}
.button {
background-color: #34302d;
background-image: none;
border-radius: 2px;
color: #f1f1f1;
font-size: 14px;
line-height: 14px;
font-family: Montserrat,sans-serif;
border: 2px solid #6db33f;
padding: 5px 20px;
text-shadow: none;
}
.button span {
background-color: #34302d;
background-image: none;
border-radius: 2px;
color: #f1f1f1;
font-size: 14px;
line-height: 14px;
font-family: Montserrat,sans-serif;
border: 2px solid #6db33f;
padding: 5px 20px;
text-shadow: none;
}
.button input {
background-color: #34302d;
background-image: none;
color: #f1f1f1;
font-size: 14px;
font-family: Montserrat,sans-serif;
text-shadow: none;
border: 0px;
text-align:right;
}
button.on {
background-color: #5fa134;
}
.flow-definition-container {
border: 1px solid;
border-color: #6db33f;
border-radius: 2px;
margin-top: 3px;
background-color: #ffffff;
font-family: monospace;
z-index: 2;
width:100%;
height:100px;
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
}
textarea:input {
outline: none;
border: 1px solid #6db33f;
}
textarea:input:focus {
outline: none;
border: 1px solid #000000;
}
.flow-definition {
border: 5px;
height:100%;
width:100%;
font-size: 16px;
resize: none;
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
}
/* The text label on the nodes */
.label {
font-family: 'Lucida Console';
font-size: 12px;
}
/* The class for the 'icon/unicode_char' on the nodes */
.label2 {
font-size: 18px;
}
[ng\:cloak], [ng-cloak], [data-ng-cloak], [x-ng-cloak], .ng-cloak, .x-ng-cloak {
display: none !important;
}

View File

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" class="collapse-handle" viewBox="0 0 24 24">
<line x1="4" y1="4" x2="20" y2="20" stroke="black" stroke-width="8" stroke-linecap="round"/>
<line x1="20" y1="4" x2="4" y2="20" stroke="black" stroke-width="8" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 296 B

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="27.963px" height="27.963px" viewBox="0 0 27.963 27.963" style="enable-background:new 0 0 27.963 27.963;"
xml:space="preserve">
<g>
<circle cx="13.9815" cy="13.9815" r="12.9815" stroke="black" stroke-width="1" fill="red" />
<polygon style="fill:white;" points="15.578,17.158 16.19,4.579 11.803,4.579 12.413,17.158 "/>
<path style="fill:white;" d="M13.997,18.546c-1.471,0-2.5,1.029-2.5,2.526c0,1.443,0.999,2.528,2.444,2.528h0.056
c1.499,0,2.469-1.085,2.469-2.528C16.44,19.575,15.467,18.546,13.997,18.546z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 791 B

View File

@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 17.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="1000px" height="1000px" viewBox="0 0 1000 1000" enable-background="new 0 0 1000 1000" xml:space="preserve">
<g>
<path d="M139.2,264.6c-47.5,71.7-72.7,155.2-72.7,241.5c0,59,11.6,116.3,34.4,170.3c22,52.1,53.6,98.9,93.7,139
c40.1,40.1,86.9,71.7,139,93.7c54,22.8,111.3,34.4,170.3,34.4s116.3-11.6,170.3-34.4c52.1-22,98.9-53.6,139-93.7
c40.1-40.1,71.7-86.9,93.7-139c22.8-54,34.4-111.3,34.4-170.3c0-50.4-8.5-99.8-25.3-146.9c-16.2-45.5-39.8-87.8-70.1-125.7
c-30-37.5-65.6-69.7-106-95.6c-41.1-26.4-86-45.6-133.4-57l-39.8,165.3c57.3,13.8,109.3,46.9,146.3,93.4
c38.1,47.8,58.3,105.4,58.3,166.7c0,71.4-27.8,138.6-78.3,189.1c-50.5,50.5-117.7,78.3-189.1,78.3s-138.6-27.8-189.1-78.3
c-50.5-50.5-78.3-117.7-78.3-189.1c0-52.8,15.3-103.8,44.3-147.6c6.5-9.9,13.7-19.2,21.4-28.1L399.1,456L508,70.1l-406.4,0.1
l95.6,124C175.7,215.5,156.2,239,139.2,264.6z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -0,0 +1,51 @@
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
<title>Spring Flo Sample</title>
<link href="http://fonts.googleapis.com/css?family=Varela+Round|Montserrat:400,700" rel="stylesheet" type="text/css" />
<link href="/webjars/spring-flo/dist/spring-flo.css" rel="stylesheet" />
<link href="css/sample.css" rel="stylesheet" />
<script data-main="js/main" src="/webjars/requirejs/require.js"></script>
</head>
<body>
<div id="header" class="header">
Spring Flo Sample
</div>
<div id="flo-container">
<flo-editor ng-cloak metamodel-service-name="SampleMetamodelService" render-service-name="SampleRenderService" editor-service-name="SampleEditorService" palette-size="170"
min-zoom="10" max-zoom="300" zoom-step="10" ng-init="canvasControls={zoom:true};">
<div id="controls" class="controls">
<button class="button" id="clearGraph" ng-click="flo.clearGraph()">Create New Flow</button>
<!-- let's use the 'x' control on the nodes themselves when they are selected
<button class="button" id="deleteSelectedNode" ng-click="flo.deleteSelectedNode()">Delete Selected Node</button>
-->
<button class="button" id="performLayout" ng-click="flo.performLayout(); flo.fitToPage();">Reset Layout</button>
<button class="button" id="sync" ng-click="flo.updateTextRepresentation()">Synchronize</button>
<button class="button" id="readOnly" ng-click="flo.readOnlyCanvas(!flo.readOnlyCanvas())" ng-class="{on:flo.readOnlyCanvas()}">Read-Only</button>
<button class="button" id="noPalette" ng-click="flo.noPalette = !flo.noPalette" ng-class="{on:!flo.noPalette}">Palette</button>
<button class="button" id="editor" ng-click="editor = !editor" ng-lcass="{on:editor}">{{editor ? 'TextArea' : 'Editor'}}</button>
<!-- Defer to using zoom control on the canvas
<span class="button">
<label>Zoom: </label>
<input id="zoomInput" type="text" ng-model="flo.zoomPercent" ng-model-options="{ getterSetter: true, updateOn: 'blur change' }" size="3"></input>
<label>%</label>
<input type="range" ng-model="flo.zoomPercent" ng-model-options="{ getterSetter: true }" step="10" max="300" min="10" data-type="range" name="range" class="range"></input>
</span>
-->
<span class="button">
<label>Grid Size: </label>
<input id="gridSizeInput" type="text" ng-model="flo.gridSize" ng-model-options="{ getterSetter: true, updateOn: 'blur change' }" size="2"></input>
<label>pixels</label>
<input type="range" ng-model="flo.gridSize" ng-model-options="{ getterSetter: true }" step="1" max="50" min="1" data-type="range" name="range" class="range"></input>
</span>
</div>
<div class="flow-definition-container">
<textarea ng-if="editor" dsl-editor="true" id="flow-definition" class="flow-definition"></textarea>
<textarea ng-if="!editor" id="flow-definition" class="flow-definition" placeholder="Enter stream definition..."
ng-model="definition.text" ng-keyup="flo.scheduleUpdateGraphRepresentation(); $event.stopPropagation();" ng-blur="flo.enableSyncing(true)" ng-focus="flo.enableSyncing(false)"></textarea>
</div>
</flo-editor>
</div>
</body>
</html>

View File

@@ -0,0 +1,505 @@
/*
* Copyright 2016 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* @author Alex Boyko
* @author Andy Clement
*/
define(function(require) {
'use strict';
var joint = require('joint');
return ['$log', function($log) {
function createHandles(flo, createHandle, element) {
var bbox = element.getBBox();
var pt = bbox.origin().offset(bbox.width + 3, bbox.height + 3);
createHandle(element, 'remove', flo.deleteSelectedNode, pt);
}
function validatePort(/*flo, cellView, magnet*/) {
return true;
}
function validateLink(flo, cellViewS, magnetS, cellViewT, magnetT/*, end, linkView*/) {
// Prevent linking from input ports.
if (magnetS && magnetS.getAttribute('type') === 'input') {
return false;
}
// Prevent linking from output ports to input ports within one element.
if (cellViewS === cellViewT) {
return false;
}
// Prevent linking to input ports.
if (magnetT && magnetT.getAttribute('type') === 'output') {
return false;
}
return cellViewS.model && cellViewT.model && !(cellViewS.model instanceof joint.shapes.flo.ErrorDecoration) && !(cellViewT.model instanceof joint.shapes.flo.ErrorDecoration);
}
function preDelete(flo, cell) {
repairDamage(flo, cell);
}
function handleNodeDropping(flo, dragDescriptor) {
var relinking = dragDescriptor.context && dragDescriptor.context.palette;
var graph = flo.getGraph();
var source = dragDescriptor.source ? dragDescriptor.source.cell : undefined;
var target = dragDescriptor.target ? dragDescriptor.target.cell : undefined;
if (target instanceof joint.dia.Element && target.attr('metadata/name')) {
var type = source.attr('metadata/name');
if (type === 'tap') {
// Fill in the channel on the new node
// work out tap name, something like: tap:stream:mystream.filter (filter optional if on head of stream)
// 1. work your way back from node that was dropped on to the first node (the one with no further links)
var oncell = target;
var headerCell = oncell;
var incomingLinks = graph.getConnectedLinks(oncell, { inbound: true });
while (incomingLinks.length !== 0) {
headerCell = graph.getCell(incomingLinks[0].get('source').id);
incomingLinks = graph.getConnectedLinks(headerCell, { inbound: true });
}
var streamname = 'STREAM';
if (headerCell.attr('stream-name')) {
streamname = headerCell.attr('stream-name');
}
else {
var streamId = headerCell.attr('stream-id');
if (streamId) {
streamname = 'STREAM'+streamId;
} else {
streamname = 'STREAM';
}
headerCell.attr('stream-name',streamname);
}
var channel = 'tap:stream:'+streamname;
var label2 = 'tap:stream:\n' + streamname;
if (oncell.id !== headerCell.id) {
channel = channel + '.' + oncell.attr('.label/text');
label2 = label2 + '.' + oncell.attr('.label/text');
}
source.attr('.label2/text', label2);
source.attr('props/channel', channel);
relinking = true;
} else {
if (dragDescriptor.target.selector === '.output-port') {
moveNodeOnNode(flo, source, target, 'right', true);
relinking = true;
} else if (dragDescriptor.target.selector === '.input-port') {
moveNodeOnNode(flo, source, target, 'left', true);
relinking = true;
}
}
} else if (target instanceof joint.dia.Link) { // jshint ignore:line
moveNodeOnLink(flo, source, target);
relinking = true;
}
// Turn off auto layout
// if (relinking) {
// flo.performLayout();
// }
}
function calculateDragDescriptor(flo, draggedView, targetUnderMouse, point, context) {
// check if it's a tap being dragged
var source = draggedView.model;
if ((targetUnderMouse instanceof joint.dia.Element) && source.attr('metadata/name') === 'tap') { // jshint ignore:line
return {
context: context,
source: {
cell: draggedView.model,
},
target: {
cell: targetUnderMouse,
}
};
}
// Find closest port
var range = 30;
var graph = flo.getGraph();
var paper = flo.getPaper();
var closestData;
var minDistance = Number.MAX_VALUE;
var maxIcomingLinks = draggedView.model.attr('metadata/constraints/maxIncomingLinksNumber');
var maxOutgoingLinks = draggedView.model.attr('metadata/constraints/maxOutgoingLinksNumber');
var hasIncomingPort = typeof maxIcomingLinks !== 'number' || maxIcomingLinks > 0;
var hasOutgoingPort = typeof maxOutgoingLinks !== 'number' || maxOutgoingLinks > 0;
if (!hasIncomingPort && !hasOutgoingPort) {
return;
}
var elements = graph.findModelsInArea(joint.g.rect(point.x - range, point.y - range, 2 * range, 2 * range)); // jshint ignore:line
if (Array.isArray(elements)) {
elements.forEach(function(model) {
var view = paper.findViewByModel(model);
if (view && view !== draggedView && model instanceof joint.dia.Element) { // jshint ignore:line
var targetMaxIcomingLinks = view.model.attr('metadata/constraints/maxIncomingLinksNumber');
var targetMaxOutgoingLinks = view.model.attr('metadata/constraints/maxOutgoingLinksNumber');
var targetHasIncomingPort = typeof targetMaxIcomingLinks !== 'number' || targetMaxIcomingLinks > 0;
var targetHasOutgoingPort = typeof targetMaxOutgoingLinks !== 'number' || targetMaxOutgoingLinks > 0;
if (view.model.attr('metadata/constraints/xorSourceSink')) {
if (targetHasIncomingPort) {
targetHasIncomingPort = targetHasIncomingPort && graph.getConnectedLinks(view.model, { outbound: true }).length === 0;
}
if (targetHasOutgoingPort) {
targetHasOutgoingPort = targetHasOutgoingPort && graph.getConnectedLinks(view.model, { inbound: true }).length === 0;
}
}
if (draggedView.model.attr('metadata/constraints/xorSourceSink')) {
if (hasIncomingPort) {
targetHasOutgoingPort = targetHasOutgoingPort && graph.getConnectedLinks(view.model, { outbound: true }).length === 0;
}
if (hasOutgoingPort) {
targetHasIncomingPort = targetHasIncomingPort && graph.getConnectedLinks(view.model, { inbound: true }).length === 0;
}
}
view.$('[magnet]').each(function(index, magnet) {
var type = magnet.getAttribute('type');
if ((type === 'input' && targetHasIncomingPort && hasOutgoingPort) || (type === 'output' && targetHasOutgoingPort && hasIncomingPort)) {
var bbox = joint.V(magnet).bbox(false, paper.viewport); // jshint ignore:line
var distance = point.distance({
x: bbox.x + bbox.width / 2,
y: bbox.y + bbox.height / 2
});
if (distance < range && distance < minDistance) {
minDistance = distance;
closestData = {
context: context,
source: {
cell: draggedView.model,
selector: type === 'output' ? '.input-port' : '.output-port'
},
target: {
cell: model,
selector: '.' + type+'-port'
},
range: minDistance
};
}
}
});
}
});
}
if (closestData) {
return closestData;
}
// Check if drop on a link is allowed
if (targetUnderMouse instanceof joint.dia.Link && !(source.attr('metadata/constraints/xorSourceSink') || source.attr('metadata/constraints/maxOutgoingLinksNumber') === 0 || source.attr('metadata/constraints/maxIncomingLinksNumber') === 0) && graph.getConnectedLinks(source).length === 0) { // jshint ignore:line
return {
context: context,
source: {
cell: source
},
target: {
cell: targetUnderMouse
}
};
}
return {
context: context,
source: {
cell: source
},
};
}
function validateNode(flo, element) {
var errors = [];
var graph = flo.getGraph();
var constraints = element.attr('metadata/constraints');
if (constraints) {
var incoming = graph.getConnectedLinks(element, {inbound: true});
var outgoing = graph.getConnectedLinks(element, {outbound: true});
if (typeof constraints.maxIncomingLinksNumber === 'number' || typeof constraints.minIncomingLinksNumber === 'number') {
if (typeof constraints.maxIncomingLinksNumber === 'number' && constraints.maxIncomingLinksNumber < incoming.length) {
if (constraints.maxIncomingLinksNumber === 0) {
errors.push({
message: 'Sources must appear at the start of a stream',
range: element.attr('range')
});
} else {
errors.push({
message: 'Max allowed number of incoming links is ' + constraints.maxIncomingLinksNumber,
range: element.attr('range')
});
}
}
if (typeof constraints.minIncomingLinksNumber === 'number' && constraints.minIncomingLinksNumber > incoming.length) {
errors.push({
message: 'Min allowed number of incoming links is ' + constraints.minIncomingLinksNumber,
range: element.attr('range')
});
}
}
if (typeof constraints.maxOutgoingLinksNumber === 'number' || typeof constraints.minOutgoingLinksNumber === 'number') {
if (typeof constraints.maxOutgoingLinksNumber === 'number' && constraints.maxOutgoingLinksNumber < outgoing.length) {
if (constraints.maxOutgoingLinksNumber === 0) {
errors.push({
message: 'Sinks must appear at the end of a stream',
range: element.attr('range')
});
} else {
errors.push({
message: 'Max allowed number of outgoing links is ' + constraints.maxOutgoingLinksNumber,
range: element.attr('range')
});
}
}
if (typeof constraints.minOutgoingLinksNumber === 'number' && constraints.minOutgoingLinksNumber > outgoing.length) {
errors.push({
message: 'Min allowed number of outgoing links is ' + constraints.minOutgoingLinksNumber,
range: element.attr('range')
});
}
}
if (constraints.xorSourceSink && incoming.length && outgoing.length) {
errors.push({
message: 'Node can either have incoming or outgoing links, but not both',
range: element.attr('range')
});
}
}
if (!element.attr('metadata') || element.attr('metadata/unresolved')) {
var msg = 'Unknown element \'' + element.attr('metadata/name') + '\'';
if (element.attr('metadata/group')) {
msg += ' from group \'' + element.attr('metadata/group') + '\'.';
}
errors.push({
message: msg,
range: element.attr('range')
});
}
// If possible, verify the properties specified match those allowed on this type of element
// propertiesRanges are the ranges for each property included the entire '--name=value'.
// The format of a range is {'start':{'ch':NNNN,'line':NNNN},'end':{'ch':NNNN,'line':NNNN}}
var propertiesRanges = element.attr('propertiesranges');
if (propertiesRanges) {
var moduleSchema = element.attr('metadata');
// Grab the list of supported properties for this module type
moduleSchema.get('properties').then(function(moduleSchemaProperties) {
if (!moduleSchemaProperties) {
moduleSchemaProperties = {};
}
// Example moduleSchemaProperties:
// {"host":{"name":"host","type":"String","description":"the hostname of the mail server","defaultValue":"localhost","hidden":false},
// "password":{"name":"password","type":"String","description":"the password to use to connect to the mail server ","defaultValue":null,"hidden":false}
var specifiedProperties = element.attr('props');
Object.keys(specifiedProperties).forEach(function(propertyName) {
if (!moduleSchemaProperties[propertyName]) {
// The schema does not mention that property
var propertyRange = propertiesRanges[propertyName];
if (propertyRange) {
errors.push({
message: 'unrecognized option \''+propertyName+'\' for module \''+element.attr('metadata/name')+'\'',
range: propertyRange
});
}
}
});
});
}
return errors;
}
function moveNodeOnNode(flo, node, pivotNode, side, shouldRepairDamage) {
side = side || 'left';
if (canSwap(flo, node, pivotNode, side)) {
var link;
var i;
if (side === 'left') {
var sources = [];
if (shouldRepairDamage) {
/*
* Commented out because it doesn't prevent cycles.
*/
// if (graph.getConnectedLinks(pivotNode, {inbound: true}).length > 0 || graph.getConnectedLinks(node, {outbound: true}).length > 0) {
repairDamage(flo, node);
// }
}
var pivotTargetLinks = flo.getGraph().getConnectedLinks(pivotNode, {inbound: true});
for (i = 0; i < pivotTargetLinks.length; i++) {
link = pivotTargetLinks[i];
sources.push(link.get('source').id);
link.remove();
}
for (i = 0; i < sources.length; i++) {
flo.createLink({
'id': sources[i],
'selector': '.output-port'
}, {
'id': node.id,
'selector': '.input-port'
});
}
flo.createLink({
'id': node.id,
'selector': '.output-port'
}, {
'id': pivotNode.id,
'selector': '.input-port'
});
} else if (side === 'right') {
var targets = [];
if (shouldRepairDamage) {
/*
* Commented out because it doesn't prevent cycles.
*/
// if (graph.getConnectedLinks(pivotNode, {outbound: true}).length > 0 || graph.getConnectedLinks(node, {inbound: true}).length > 0) {
repairDamage(flo, node);
// }
}
var pivotSourceLinks = flo.getGraph().getConnectedLinks(pivotNode, {outbound: true});
for (i = 0; i < pivotSourceLinks.length; i++) {
link = pivotSourceLinks[i];
targets.push(link.get('target').id);
link.remove();
}
for (i = 0; i < targets.length; i++) {
flo.createLink({
'id': node.id,
'selector': '.output-port'
}, {
'id': targets[i],
'selector': '.input-port'
});
}
flo.createLink({
'id': pivotNode.id,
'selector': '.output-port'
}, {
'id': node.id,
'selector': '.input-port'
});
}
}
}
function moveNodeOnLink(flo, node, link, shouldRepairDamage) {
var source = link.get('source').id;
var target = link.get('target').id;
if (shouldRepairDamage) {
repairDamage(flo, node);
}
link.remove();
if (source) {
flo.createLink({
'id': source,
'selector': '.output-port'
}, {
'id': node.id,
'selector': '.input-port'
});
}
if (target) {
flo.createLink({
'id': node.id,
'selector': '.output-port'
}, {
'id': target,
'selector': '.input-port'
});
}
}
function repairDamage(flo, node) {
/*
* remove incoming, outgoing links and cache their sources and targets not equal to current node
*/
var sources = [];
var targets = [];
var i = 0;
var links = flo.getGraph().getConnectedLinks(node);
for (i = 0; i < links.length; i++) {
var targetId = links[i].get('target').id;
var sourceId = links[i].get('source').id;
if (targetId === node.id) {
links[i].remove();
sources.push(sourceId);
} else if (sourceId === node.id) {
links[i].remove();
targets.push(targetId);
}
}
/*
* best attempt to connect source and targets bypassing the node
*/
if (sources.length === 1) {
var source = sources[0];
for (i = 0; i < targets.length; i++) {
flo.createLink({
'id': source,
'selector': '.output-port'
}, {
'id': targets[i],
'selector': '.input-port'
});
}
} else if (targets.length === 1) {
var target = targets[0];
for (i = 0; i < sources.length; i++) {
flo.createLink({
'id': sources[i],
'selector': '.output-port'
}, {
'id': target,
'selector': '.input-port'
});
}
}
}
/**
* Check if node being dropped and drop target node next to each other such that they won't be swapped by the drop
*/
function canSwap(flo, dropee, target, side) {
var i, targetId, sourceId, noSwap = (dropee.id === target.id);
if (dropee === target) {
$log.debug('What!??? Dragged == Dropped!!! id = ' + target);
}
var links = flo.getGraph().getConnectedLinks(dropee);
for (i = 0; i < links.length && !noSwap; i++) {
targetId = links[i].get('target').id;
sourceId = links[i].get('source').id;
noSwap = (side === 'left' && targetId === target.id && sourceId === dropee.id) || (side === 'right' && targetId === dropee.id && sourceId === target.id);
}
return !noSwap;
}
return {
'createHandles': createHandles,
'validatePort': validatePort,
'validateLink': validateLink,
'calculateDragDescriptor': calculateDragDescriptor,
'handleNodeDropping': handleNodeDropping,
'validateNode': validateNode,
'preDelete': preDelete,
'interactive': {
'vertexAdd': false
},
'allowLinkVertexEdit': false
};
}];
});

View File

@@ -0,0 +1,179 @@
/*
* Copyright 2016 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* Convert a graph to a text representation.
*
* @author Alex Boyko
* @author Andy Clement
*/
define(function() {
'use strict';
// Graph
var g;
// Number of Links left to visit
var numberOfLinksToVisit;
// Number of nodes left to visit
var numberOfNodesToVisit;
// Map of links left to visit indexed by id
var linksToVisit;
// Map of nodes left to visit indexed by id
var nodesToVisit;
// Map of nodes incoming non-visited links degrees index by node id
var nodesInDegrees;
// Priority:
// 1. find links whose source has no other links pointing at it
// 2. find links whose source has already been processed (not currently needed in sample DSL since
// can't create graphs like that due to metamodel constraints)
// 3. find remaining links
function nextLink() {
var indegree = Number.MAX_INT;
var currentBest;
for (var id in linksToVisit) {
var link = g.getCell(id);
var source = g.getCell(link.get('source').id);
var currentInDegree = nodesInDegrees[source.get('id')];
if (currentInDegree === 0) {
return visit(link);
} else if (indegree > currentInDegree) {
indegree = currentInDegree;
currentBest = link;
}
}
if (currentBest) {
return visit(currentBest);
}
}
function visit(e) {
if (e.isLink()) {
delete linksToVisit[e.get('id')];
nodesInDegrees[e.get('target').id]--;
numberOfLinksToVisit--;
} else {
delete nodesToVisit[e.get('id')];
numberOfNodesToVisit--;
}
return e;
}
function init(graph) {
numberOfLinksToVisit = 0;
numberOfNodesToVisit = 0;
linksToVisit = {};
nodesToVisit = {};
nodesInDegrees = {};
g = graph;
g.getElements().forEach(function(element) {
if (element.attr('metadata/name')) { // is it a node?
nodesToVisit[element.get('id')] = element;
var indegree = 0;
g.getConnectedLinks(element, {inbound: true}).forEach(function(link) {
if (link.get('source') && link.get('source').id && g.getCell(link.get('source').id) &&
g.getCell(link.get('source').id).attr('metadata/name')) {
linksToVisit[link.get('id')] = link;
numberOfLinksToVisit++;
indegree++;
}
});
nodesInDegrees[element.get('id')] = indegree;
numberOfNodesToVisit++;
}
});
}
/**
* Starts at a link and proceeds down a chain. Converts each node to
* text and then joins them with a ' > '.
*/
function chainToText(link) {
var text = '';
var source = g.getCell(link.get('source').id);
text += nodeToText(source, true);
while (link) {
var target = g.getCell(link.get('target').id);
text += ' > ';
text += nodeToText(target, false);
// Find next not visited link to follow
link = null;
var outgoingLinks = g.getConnectedLinks(target, {outbound: true});
for (var i = 0; i < outgoingLinks.length && !link; i++) {
if (linksToVisit[outgoingLinks[i].get('id')]) {
source = target;
link = visit(outgoingLinks[i]);
}
}
}
return text;
}
/**
* Very basic format. From a node to the text:
* "name --key=value --key=value"
*/
function nodeToText(element) {
var text = '';
var props = element.attr('props');
if (!element) {
return;
}
text += element.attr('metadata/name');
if (props) {
Object.keys(props).forEach(function(propertyName) {
text += ' --' + propertyName + '=' + props[propertyName];
});
}
visit(element);
return text;
}
function appendChainText(text, chainText) {
if (chainText) {
if (text) {
text += '\n';
}
text += chainText;
}
return text;
}
// Translate a graph into a basic string
return function(g) {
var text = '';
var chainText;
var id;
init(g);
while (numberOfLinksToVisit) {
chainText = chainToText(nextLink());
text = appendChainText(text, chainText);
}
// Visit all disconnected nodes
for (id in nodesToVisit) {
chainText = nodeToText(nodesToVisit[id], true);
text = appendChainText(text, chainText);
}
return text;
};
});

View File

@@ -0,0 +1,83 @@
/*
* Copyright 2016 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* @author Alex Boyko
* @author Andy Clement
*/
requirejs.config({
baseUrl:'js',
paths: {
joint: '/webjars/jointjs/dist/joint',
backbone: '/webjars/backbone/backbone',
domReady: '/webjars/requirejs-domready/domReady',
angular: '/webjars/angular/angular',
jquery: '/webjars/jquery/dist/jquery',
bootstrap:'/webjars/bootstrap/bootstrap',
lodash: '/webjars/lodash/lodash', // lodash.compat
dagre: '/webjars/dagre/dist/dagre.core',
graphlib: '/webjars/graphlib/graphlib.core',
text : '/webjars/requirejs-text/text',
flo : '/webjars/spring-flo/dist/spring-flo',
json5 : '/webjars/json5/json5'
},
map: {
'*': {
// Backbone requires underscore. This forces requireJS to load lodash instead:
'underscore': 'lodash'
}
},
packages: [
{
name: 'codemirror',
location: '../lib/codemirror',
main: 'lib/codemirror'
}
],
shim: {
angular: {
deps: ['bootstrap'],
exports: 'angular'
},
bootstrap: {
deps: ['jquery']
},
graphlib: {
deps: ['underscore']
},
dagre: {
deps: ['graphlib', 'underscore']
},
joint: {
deps: ['jquery', 'underscore', 'backbone'],
},
underscore: {
exports: '_'
},
'flo': {
deps: ['angular', 'jquery', 'joint', 'underscore']
}
}
});
define(['require','angular'], function (require, angular) {
'use strict';
require(['domReady!', 'sample-app'],
function (document) {
angular.bootstrap(document, ['floSampleApp']);
}
);
});

View File

@@ -0,0 +1,46 @@
[{
'name':'http', 'group':'source', 'description':'Receive HTTP input',
'properties':[
{ 'id':'port','name':'port', 'defaultValue':'80', 'description':'Port on which to listen' }
],
'constraints':{ 'maxIncomingLinksNumber':0, 'maxOutgoingLinksNumber':1 }
},{
'name':'rabbit', 'group':'source', 'description':'Receives messages from RabbitMQ',
'properties':[
{ 'id':'queue','name':'queue', 'description':'the queue(s) from which messages will be received' }
],
'constraints':{ 'maxIncomingLinksNumber':0, 'maxOutgoingLinksNumber':1 }
},{
'name':'filewatch', 'group':'source', 'description':'Produce messages from the content of files created in a directory',
'properties':[
{'id':'dir','name':'dir','description':'the absolute path to monitor for files'}
],
'constraints':{ 'maxIncomingLinksNumber':0, 'maxOutgoingLinksNumber':1 }
},{
'name':'transform', 'group':'processor', 'description':'Apply an expression to modify incoming messages',
'properties':[
{ 'id':'expression','name':'expression', 'defaultValue':'payload', 'description':'SpEL expression to apply' }
],
'constraints':{ 'maxIncomingLinksNumber':1, 'maxOutgoingLinksNumber':1 }
},{
'name':'filter', 'group':'processor', 'description':'Only allow messages through that pass the filter expression',
'properties':[
{ 'id':'expression','name':'expression', 'defaultValue':'true', 'description':'SpEL expression to use for filtering' }
],
'constraints':{ 'maxIncomingLinksNumber':1, 'maxOutgoingLinksNumber':1 }
},{
'name':'filesave', 'group':'sink', 'description':'Writes messages to a file',
'properties':[
{ 'id':'dir','name':'dir', 'description':'Absolute path to directory' },
{ 'id':'name','name':'name', 'description':'The name of the file to create' }
],
'constraints':{ 'maxIncomingLinksNumber':1, 'maxOutgoingLinksNumber':0 }
},{
'name':'ftp', 'group':'sink', 'description':'Send messages over FTP',
'properties':[
{ 'id':'host','name':'host', 'description':'the host name for the FTP server' },
{ 'id':'port','name':'port', 'description':'The port for the FTP server' },
{ 'id':'remoteDir','name':'remoteDir', 'description':'The remote directory on the server' },
],
'constraints':{ 'maxIncomingLinksNumber':1, 'maxOutgoingLinksNumber':0 }
}]

View File

@@ -0,0 +1,114 @@
/*
* Copyright 2016 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* @author Alex Boyko
* @author Andy Clement
*/
define(function(require) {
'use strict';
require('json5');
var convertGraphToText = require('graph-to-text');
var convertTextToGraph = require('text-to-graph');
return ['$http', '$q', '$timeout', '$log', 'MetamodelUtils', function($http, $q, $timeout, $log, metamodelUtils) {
var metamodel;
// Internally stored metamodel load promise
var request;
/**
* Helper that goes from basic JSON to a lazy getter structure. Useful when the
* metamodel is 'cheap' to build. If it is costly to discover the actual properties
* the getter may be more complex (e.g. make a REST request).
*/
function createMetadata(entry) {
var props = {};
if (Array.isArray(entry.properties)) {
entry.properties.forEach(function(property) {
if (!property.id) {
property.id = property.name;
}
props[property.id] = property;
});
}
entry.properties = props;
return {
name: entry.name,
group: entry.group,
icon: entry.icon,
constraints: entry.constraints,
description: entry.description,
metadata: entry.metadata,
properties: entry.properties,
get: function(property) {
var deferred = $q.defer();
if (entry.hasOwnProperty(property)) {
deferred.resolve(entry[property]);
} else {
deferred.reject();
}
return deferred.promise;
}
};
}
function load() {
// COULDDO: to cache the result here, check result before doing this processing
// and simply return it if it is set. If doing that may want to override refresh
// in this service
var metamodelData = JSON5.parse(require('text!metamodel-sample.json'));
var deferred = $q.defer();
var newData = {};
metamodelData.forEach(function(data) {
var metadata = createMetadata(data);
if (!newData[metadata.group]) {
newData[metadata.group] = {};
}
newData[metadata.group][metadata.name] = metadata;
});
metamodel = newData;
deferred.resolve(metamodel);
request = deferred.promise;
return request;
}
function graphToText(flo, definition) {
definition.text = convertGraphToText(flo.getGraph());
}
function textToGraph(flo, definition) {
// TODO perhaps push these flo operations into the 'caller' to make this simpler
flo.getGraph().clear();
load().then(function(metamodel) {
convertTextToGraph(definition.text, flo, metamodel, metamodelUtils);
flo.performLayout();
flo.fitToPage();
});
}
return {
'load': load,
'textToGraph': textToGraph,
'graphToText': graphToText
};
}];
});

View File

@@ -0,0 +1,206 @@
/*
* Copyright 2016 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* @author Alex Boyko
* @author Andy Clement
*/
define(function(require) {
'use strict';
var joint = require('joint');
var dagre = require('dagre');
var HANDLE_ICON_MAP = {
'remove': 'icons/delete.svg',
};
var DECORATION_ICON_MAP = {
'error': 'icons/error.svg'
};
return ['$log', function($log) {
function createHandle(kind) {
return new joint.shapes.flo.ErrorDecoration({
size: {width: 10, height: 10},
attrs: {
'image': {
'xlink:href': HANDLE_ICON_MAP[kind]
}
}
});
}
function createDecoration(kind) {
return new joint.shapes.flo.ErrorDecoration({
size: {width: 16, height: 16},
attrs: {
'image': {
'xlink:href': DECORATION_ICON_MAP[kind]
}
}
});
}
function createNode(/*metadata, props*/) {
return new joint.shapes.flo.Node();
}
function initializeNewNode(node, context) {
var metadata = node.attr('metadata');
if (metadata) {
node.attr('.label/text', node.attr('metadata/name'));
if (node.attr('metadata/constraints/maxIncomingLinksNumber') === 0) {
node.attr('.input-port/display','none');
}
if (node.attr('metadata/constraints/maxOutgoingLinksNumber') === 0) {
node.attr('.output-port/display','none');
}
var type = node.attr('metadata/name');
if (type === 'tap') {
if (!node.attr('props/channel')) {
node.attr('props/channel', 'tap:stream:STREAM');
}
refreshVisuals(node, 'props/channel', context.paper);
} else if (type === 'named-channel') {
// Default channel for named channel is 'queue:default'
if (!node.attr('props/channel')) {
node.attr('props/channel', 'queue:default');
}
refreshVisuals(node, 'props/channel', context.paper);
}
}
}
function createLink() {
var link = new joint.shapes.flo.Link(joint.util.deepSupplement({
smooth: true,
attrs: {
'.': {
//filter: { name: 'dropShadow', args: { dx: 1, dy: 1, blur: 2 } }
},
'.connection': { 'stroke-width': 3, 'stroke': 'black', 'stroke-linecap': 'round' },
'.marker-arrowheads': { display: 'none' },
'.tool-options': { display: 'none' }
},
}, joint.shapes.flo.Link.prototype.defaults));
return link;
}
function isSemanticProperty(propertyPath) {
return propertyPath === '.label/text';
}
function refreshVisuals(element, changedPropertyPath/*, paper*/) {
var type = element.attr('metadata/name');
if (type === 'tap' && changedPropertyPath === 'props/channel') {
var name = element.attr('props/channel');
if (name) {
var colon = name.indexOf(':');
if (colon !== -1) {
colon = name.indexOf(':',colon+1);
if (colon !== -1) {
var displayValue = name.substring(0,colon) + '\n' + name.substring(colon);
element.attr('.label2/text', displayValue);
}
}
} else {
element.removeAttr('.label2/text');
}
}
if (type === 'named-channel' && changedPropertyPath === 'props/channel') {
element.attr('.label2/text', element.attr('props/channel'));
}
}
function layout(paper) {
var graph = paper.model;
var i;
var g = new dagre.graphlib.Graph();
g.setGraph({});
g.setDefaultEdgeLabel(function() {return{};});
var nodes = graph.getElements();
for (i = 0; i < nodes.length; i++) {
var node = nodes[i];
if (node.get('type') === joint.shapes.flo.NODE_TYPE) {
g.setNode(node.id, node.get('size'));
}
}
var links = graph.getLinks();
for (i = 0; i < links.length; i++) {
var link = links[i];
if (link.get('type') === joint.shapes.flo.LINK_TYPE) {
var options = {
minlen: 1.5
};
// if (link.get('labels') && link.get('labels').length > 0) {
// options.minlen = 1 + link.get('labels').length * 0.5;
// }
g.setEdge(link.get('source').id, link.get('target').id, options);
link.set('vertices', []);
}
}
g.graph().rankdir = 'LR';
dagre.layout(g);
g.nodes().forEach(function(v) {
var node = graph.getCell(v);
if (node) {
var bbox = node.getBBox();
node.translate(g.node(v).x - bbox.x, g.node(v).y - bbox.y);
}
});
}
function getLinkAnchorPoint(linkView, view, magnet, reference) {
if (magnet) {
var type = magnet.getAttribute('type');
var bbox = joint.V(magnet).bbox(false, linkView.paper.viewport);
var rect = joint.g.rect(bbox);
if (type === 'input') {
return joint.g.point(rect.x, rect.y + rect.height / 2);
} else {
return joint.g.point(rect.x + rect.width, rect.y + rect.height / 2);
}
} else {
$log.debug('No magnet!');
return reference;
}
}
return {
'createHandle': createHandle,
'createDecoration': createDecoration,
'createNode': createNode,
'createLink': createLink,
'initializeNewNode': initializeNewNode,
'isSemanticProperty': isSemanticProperty,
'refreshVisuals': refreshVisuals,
'layout': layout,
'getLinkAnchorPoint': getLinkAnchorPoint
};
}];
});

View File

@@ -0,0 +1,30 @@
/*
* Copyright 2016 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* @author Alex Boyko
* @author Andy Clement
*/
define(function(require) {
'use strict';
var angular = require('angular');
require('flo');
var app = angular.module('floSampleApp', [ 'spring.flo' ]);
app.factory('SampleMetamodelService', require('metamodel-service'));
app.factory('SampleRenderService', require('render-service'));
app.factory('SampleEditorService', require('editor-service'));
return app;
});

View File

@@ -0,0 +1,71 @@
/*
* Copyright 2016 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* Convert a text representation to a graph.
*
* @author Alex Boyko
* @author Andy Clement
*/
define(function() {
'use strict';
return function(input, flo, metamodel, metamodelUtils) {
// input is a string like this (3 nodes: foo, goo and hoo): foo --a=b --c=d > goo --d=e --f=g>hoo
var trimmed = input.trim();
if (trimmed.length===0) {
return;
}
var lines = trimmed.split('\n');
for (var l=0;l<lines.length;l++) {
var line = lines[l];
var elements = line.split('>');
var lastNode = null;
for (var e=0;e<elements.length;e++) {
var element = elements[e].trim();
// Has properties?
var startOfProps = element.indexOf(' ');
var name = element;
var properties = {};
if (startOfProps !== -1) {
name = element.substring(0,startOfProps);
var propValues = element.substring(startOfProps+1).trim().split(' ');
for (var p=0;p<propValues.length;p++) {
var propValue = propValues[p].trim();
if (propValue.length===0) {
// allows for multiple spaces between options
continue;
}
var equalsIndex = propValue.indexOf('=');
// The 2 skips the '--'
var key = propValue.substring(2,equalsIndex);
var value = propValue.substring(equalsIndex+1);
properties[key] = value;
}
}
var group = metamodelUtils.matchGroup(metamodel, name, 1, 1);
var newNode = flo.createNode(metamodelUtils.getMetadata(metamodel,name,group),properties);
newNode.attr('.label/text',name);
if (lastNode) {
flo.createLink({'id': lastNode.id,'selector': '.output-port'},
{'id': newNode.id,'selector': '.input-port'});
}
lastNode = newNode;
}
}
};
});