348 lines
15 KiB
HTML
348 lines
15 KiB
HTML
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<title>System Dynamics</title>
|
|
<meta name="description" content="A simple implementation of a system dynamics editor." />
|
|
<!-- Copyright 1998-2017 by Northwoods Software Corporation. -->
|
|
<meta charset="UTF-8">
|
|
<style>
|
|
button {}
|
|
.pointer_normal { font-weight: normal; background: #f0f0f0; }
|
|
.pointer_selected { font-weight: bold; background: yellow; }
|
|
.node_normal { font-weight: normal; background: #fff0f0; }
|
|
.node_selected { font-weight: bold; background: #ff8080; }
|
|
.link_normal { font-weight: normal; background: #f0fff0; }
|
|
.link_selected { font-weight: bold; background: #80ff80; }
|
|
</style>
|
|
<script src="../release/go.js"></script>
|
|
<script src="../extensions/NodeLabelDraggingTool.js"></script>
|
|
<script src="../assets/js/goSamples.js"></script> <!-- this is only for the GoJS Samples framework -->
|
|
<script id="code">
|
|
// SD is a global variable, to avoid polluting global namespace and to make the global
|
|
// nature of the individual variables obvious.
|
|
var SD = {
|
|
mode: "pointer", // Set to default mode. Alternatives are "node" and "link", for
|
|
// adding a new node or a new link respectively.
|
|
itemType: "pointer", // Set when user clicks on a node or link button.
|
|
nodeCounter: {stock:0, cloud:0, variable:0, valve:0}
|
|
};
|
|
var myDiagram; // Declared as global
|
|
|
|
function init() {
|
|
if (window.goSamples) goSamples(); // init for these samples -- you don't need to call this
|
|
var $ = go.GraphObject.make;
|
|
|
|
myDiagram = $(go.Diagram, "myDiagram",
|
|
{
|
|
initialContentAlignment: go.Spot.Center,
|
|
"undoManager.isEnabled": true,
|
|
allowLink: false, // linking is only started via buttons, not modelessly
|
|
"animationManager.isEnabled": false,
|
|
|
|
"linkingTool.portGravity": 0, // no snapping while drawing new links
|
|
"linkingTool.doActivate": function() {
|
|
// change the curve of the LinkingTool.temporaryLink
|
|
this.temporaryLink.curve = (SD.itemType === "flow") ? go.Link.Normal : go.Link.Bezier;
|
|
this.temporaryLink.path.stroke = (SD.itemType === "flow") ? "blue" : "green";
|
|
this.temporaryLink.path.strokeWidth = (SD.itemType === "flow") ? 5 : 1;
|
|
go.LinkingTool.prototype.doActivate.call(this);
|
|
},
|
|
// override the link creation process
|
|
"linkingTool.insertLink": function(fromnode, fromport, tonode, toport) {
|
|
// to control what kind of Link is created,
|
|
// change the LinkingTool.archetypeLinkData's category
|
|
myDiagram.model.setCategoryForLinkData(this.archetypeLinkData, SD.itemType);
|
|
// Whenever a new Link is drawng by the LinkingTool, it also adds a node data object
|
|
// that acts as the label node for the link, to allow links to be drawn to/from the link.
|
|
this.archetypeLabelNodeData = (SD.itemType === "flow") ? { category: "valve" } : null;
|
|
// also change the text indicating the condition, which the user can edit
|
|
this.archetypeLinkData.text = SD.itemType;
|
|
return go.LinkingTool.prototype.insertLink.call(this, fromnode, fromport, tonode, toport);
|
|
},
|
|
|
|
"clickCreatingTool.archetypeNodeData": {}, // enable ClickCreatingTool
|
|
"clickCreatingTool.isDoubleClick": false, // operates on a single click in background
|
|
"clickCreatingTool.canStart": function() { // but only in "node" creation mode
|
|
return SD.mode === "node" && go.ClickCreatingTool.prototype.canStart.call(this);
|
|
},
|
|
"clickCreatingTool.insertPart": function(loc) { // customize the data for the new node
|
|
SD.nodeCounter[SD.itemType] += 1;
|
|
var newNodeId = SD.itemType + SD.nodeCounter[SD.itemType];
|
|
this.archetypeNodeData = {
|
|
key: newNodeId,
|
|
category: SD.itemType,
|
|
label: newNodeId
|
|
};
|
|
return go.ClickCreatingTool.prototype.insertPart.call(this, loc);
|
|
}
|
|
});
|
|
|
|
// install the NodeLabelDraggingTool as a "mouse move" tool
|
|
myDiagram.toolManager.mouseMoveTools.insertAt(0, new NodeLabelDraggingTool());
|
|
|
|
// when the document is modified, add a "*" to the title and enable the "Save" button
|
|
myDiagram.addDiagramListener("Modified", function(e) {
|
|
var button = document.getElementById("SaveButton");
|
|
if (button) button.disabled = !myDiagram.isModified;
|
|
var idx = document.title.indexOf("*");
|
|
if (myDiagram.isModified) {
|
|
if (idx < 0) document.title += "*";
|
|
} else {
|
|
if (idx >= 0) document.title = document.title.substr(0, idx);
|
|
}
|
|
});
|
|
|
|
// generate unique label for valve on newly-created flow link
|
|
myDiagram.addDiagramListener("LinkDrawn", function(e) {
|
|
var link = e.subject;
|
|
if (link.category === "flow") {
|
|
myDiagram.startTransaction("updateNode");
|
|
SD.nodeCounter.valve += 1;
|
|
var newNodeId = "flow" + SD.nodeCounter.valve;
|
|
var labelNode = link.labelNodes.first();
|
|
myDiagram.model.setDataProperty(labelNode.data, "label", newNodeId);
|
|
myDiagram.commitTransaction("updateNode");
|
|
}
|
|
});
|
|
|
|
buildTemplates();
|
|
|
|
load();
|
|
}
|
|
|
|
function buildTemplates() {
|
|
var $ = go.GraphObject.make;
|
|
|
|
// helper functions for the templates
|
|
function nodeStyle() {
|
|
return [
|
|
{
|
|
type: go.Panel.Spot,
|
|
layerName: "Background",
|
|
locationObjectName: "SHAPE",
|
|
selectionObjectName: "SHAPE",
|
|
locationSpot: go.Spot.Center
|
|
},
|
|
new go.Binding("location", "loc", go.Point.parse).makeTwoWay(go.Point.stringify)
|
|
];
|
|
}
|
|
|
|
function shapeStyle() {
|
|
return {
|
|
name: "SHAPE",
|
|
stroke: "black",
|
|
fill: "#f0f0f0",
|
|
portId: "", // So a link can be dragged from the Node: see /GraphObject.html#portId
|
|
fromLinkable: true,
|
|
toLinkable: true
|
|
};
|
|
}
|
|
|
|
function textStyle() {
|
|
return [
|
|
{
|
|
font: "bold 11pt helvetica, bold arial, sans-serif",
|
|
margin: 2,
|
|
editable: true
|
|
},
|
|
new go.Binding("text", "label").makeTwoWay()
|
|
];
|
|
}
|
|
|
|
// Node templates
|
|
myDiagram.nodeTemplateMap.add("stock",
|
|
$(go.Node, nodeStyle(),
|
|
$(go.Shape, shapeStyle(),
|
|
{ desiredSize: new go.Size(50, 30) }),
|
|
$(go.TextBlock, textStyle(),
|
|
{
|
|
_isNodeLabel: true, // declare draggable by NodeLabelDraggingTool
|
|
alignment: new go.Spot(0.5, 0.5, 0, 30) // initial value
|
|
},
|
|
new go.Binding("alignment", "label_offset", go.Spot.parse).makeTwoWay(go.Spot.stringify))
|
|
));
|
|
|
|
myDiagram.nodeTemplateMap.add("cloud",
|
|
$(go.Node, nodeStyle(),
|
|
$(go.Shape, shapeStyle(),
|
|
{
|
|
figure: "Cloud",
|
|
desiredSize: new go.Size(35, 35)
|
|
})
|
|
));
|
|
|
|
myDiagram.nodeTemplateMap.add("valve",
|
|
$(go.Node, nodeStyle(),
|
|
{
|
|
movable: false,
|
|
layerName: "Foreground",
|
|
alignmentFocus: go.Spot.None
|
|
},
|
|
$(go.Shape, shapeStyle(),
|
|
{
|
|
figure: "Ellipse",
|
|
desiredSize: new go.Size(20, 20)
|
|
}),
|
|
$(go.TextBlock, textStyle(),
|
|
{
|
|
_isNodeLabel: true, // declare draggable by NodeLabelDraggingTool
|
|
alignment: new go.Spot(0.5, 0.5, 0, 20) // initial value
|
|
},
|
|
new go.Binding("alignment", "label_offset", go.Spot.parse).makeTwoWay(go.Spot.stringify))
|
|
));
|
|
|
|
myDiagram.nodeTemplateMap.add("variable",
|
|
$(go.Node, nodeStyle(),
|
|
{ type: go.Panel.Auto },
|
|
$(go.TextBlock, textStyle(),
|
|
{ isMultiline: false }),
|
|
$(go.Shape, shapeStyle(),
|
|
// the port is in front and transparent, even though it goes around the text;
|
|
// in "link" mode will support drawing a new link
|
|
{ isPanelMain: true, stroke: null, fill: "transparent" })
|
|
));
|
|
|
|
// Link templates
|
|
myDiagram.linkTemplateMap.add("flow",
|
|
$(go.Link,
|
|
{ toShortLength: 8 },
|
|
$(go.Shape,
|
|
{ stroke: "blue", strokeWidth: 5 }),
|
|
$(go.Shape,
|
|
{
|
|
fill: "blue",
|
|
stroke: null,
|
|
toArrow: "Standard",
|
|
scale: 2.5
|
|
})
|
|
));
|
|
|
|
myDiagram.linkTemplateMap.add("influence",
|
|
$(go.Link,
|
|
{ curve: go.Link.Bezier, toShortLength: 8 },
|
|
$(go.Shape,
|
|
{ stroke: "green", strokeWidth: 1.5 }),
|
|
$(go.Shape,
|
|
{
|
|
fill: "green",
|
|
stroke: null,
|
|
toArrow: "Standard",
|
|
scale: 1.5
|
|
})
|
|
));
|
|
}
|
|
|
|
function setMode(mode, itemType) {
|
|
myDiagram.startTransaction();
|
|
document.getElementById(SD.itemType + "_button").className = SD.mode + "_normal";
|
|
document.getElementById(itemType + "_button").className = mode + "_selected";
|
|
SD.mode = mode;
|
|
SD.itemType = itemType;
|
|
if (mode === "pointer") {
|
|
myDiagram.allowLink = false;
|
|
myDiagram.nodes.each(function(n) { n.port.cursor = ""; });
|
|
} else if (mode === "node") {
|
|
myDiagram.allowLink = false;
|
|
myDiagram.nodes.each(function(n) { n.port.cursor = ""; });
|
|
} else if (mode === "link") {
|
|
myDiagram.allowLink = true;
|
|
myDiagram.nodes.each(function(n) { n.port.cursor = "pointer"; });
|
|
}
|
|
myDiagram.commitTransaction("mode changed");
|
|
}
|
|
|
|
// Show the diagram's model in JSON format that the user may edit
|
|
function save() {
|
|
document.getElementById("mySavedModel").value = myDiagram.model.toJson();
|
|
myDiagram.isModified = false;
|
|
}
|
|
function load() {
|
|
myDiagram.model = go.Model.fromJson(document.getElementById("mySavedModel").value);
|
|
}
|
|
</script>
|
|
</head>
|
|
<body onload="init()">
|
|
<div id="sample">
|
|
<div id="myDiagram" style="width:600px; height:500px; border:solid 1px black"></div>
|
|
<button id="pointer_button" class="pointer_selected" onclick="setMode('pointer','pointer');">Pointer</button>
|
|
<button id="stock_button" class="node_normal" onclick="setMode('node','stock');" style="margin-left:20px;">Stock</button>
|
|
<button id="cloud_button" class="node_normal" onclick="setMode('node','cloud');">Cloud</button>
|
|
<button id="variable_button" class="node_normal" onclick="setMode('node','variable');">Variable</button>
|
|
<button id="flow_button" class="link_normal" onclick="setMode('link','flow');" style="margin-left:20px;">Flow</button>
|
|
<button id="influence_button" class="link_normal" onclick="setMode('link','influence');">Influence</button>
|
|
<p>
|
|
A <em>system dynamics diagram</em> shows the storages (stocks) and flows of material in some system,
|
|
and the factors that influence the rates of flow.
|
|
It is usually a cosmetic interface for building mathematical models --
|
|
you provide values and equations for the stocks and flows,
|
|
and appropriate software can then simulate the system's behaiour.
|
|
</p>
|
|
<p>
|
|
The diagram has two types of link: flow links and influence links.
|
|
In additon to the node attached to each flow, there are 3 types of node:
|
|
<ul>
|
|
<li><b>stocks</b>, the amount of some substance</li>
|
|
<li><b>clouds</b>, like stocks, but outside the system of interest</li>
|
|
<li><b>variables</b>, either numeric constants or calculated from other elements</li>
|
|
</ul>
|
|
</p>
|
|
<p>
|
|
The conventional user interface for building system dynamics diagrams is modal --
|
|
you select a tool in the toolbar, then either click in an empty part of the diagram to add a node
|
|
or drag from one node to another to add a link.
|
|
That is the approach used in this example, accomplished with the <a>clickCreatingTool</a> and <a>linkingTool</a>.
|
|
Note that you need to click on the Pointer tool to revert to the normal mode.
|
|
</p>
|
|
<p>
|
|
In addition to the above, the diagram also installs the <a href="../extensions/NodeLabelDragging.html">NodeLabelDraggingTool</a>
|
|
extension into <a>ToolManager.mouseMoveTools</a>.
|
|
</p>
|
|
<p>
|
|
This sample is based on a prototype developed by Robert Muetzelfeldt.
|
|
</p>
|
|
<div>
|
|
<div>
|
|
<button id="SaveButton" onclick="save()">Save</button>
|
|
<button onclick="load()">Load</button>
|
|
Diagram Model saved in JSON format:
|
|
</div>
|
|
<textarea id="mySavedModel" style="width:100%; height:400px">
|
|
{ "class": "go.GraphLinksModel",
|
|
"linkLabelKeysProperty": "labelKeys",
|
|
"nodeDataArray": [
|
|
{"key":"grass", "category":"stock", "label":"Grass", "loc":"30 220", "label_offset":"0.5 0.5 0 30"},
|
|
{"key":"cloud1", "category":"cloud", "loc":"200 220"},
|
|
{"key":"sheep", "category":"stock", "label":"Sheep", "loc":"30 20","label_offset":"0.5 0.5 0 -30"},
|
|
{"key":"cloud2", "category":"cloud", "loc":"200 20"},
|
|
{"key":"cloud3", "category":"cloud", "loc":"-150 220"},
|
|
{"key":"grass_loss", "category":"valve", "label":"grass_loss","label_offset":"0.5 0.5 0 20" },
|
|
{"key":"grazing", "category":"valve", "label":"grazing","label_offset":"0.5 0.5 45 0" },
|
|
{"key":"growth", "category":"valve", "label":"growth","label_offset":"0.5 0.5 0 20" },
|
|
{"key":"sheep_loss", "category":"valve", "label":"sheep_loss","label_offset":"0.5 0.5 0 20" },
|
|
{"key":"k1", "category":"variable", "label":"good weather", "loc": "-80 100"},
|
|
{"key":"k2", "category":"variable", "label":"bad weather", "loc": "100 150"},
|
|
{"key":"k3", "category":"variable", "label":"wolves", "loc": "150 -40"}
|
|
],
|
|
"linkDataArray": [
|
|
{"from":"grass", "to":"cloud1", "category":"flow", "labelKeys":[ "grass_loss" ]},
|
|
{"from":"sheep", "to":"cloud2", "category":"flow", "labelKeys":[ "sheep_loss" ]},
|
|
{"from":"grass", "to":"sheep", "category":"flow", "labelKeys":[ "grazing" ]},
|
|
{"from":"cloud3", "to":"grass", "category":"flow", "labelKeys":[ "growth" ]},
|
|
{"from":"grass", "to":"grass_loss", "category":"influence"},
|
|
{"from":"sheep", "to":"sheep_loss", "category":"influence"},
|
|
{"from":"grass", "to":"growth", "category":"influence"},
|
|
{"from":"grass", "to":"grazing", "category":"influence"},
|
|
{"from":"sheep", "to":"grazing", "category":"influence"},
|
|
{"from":"k1", "to":"growth", "category":"influence"},
|
|
{"from":"k1", "to":"grazing", "category":"influence"},
|
|
{"from":"k2", "to":"grass_loss", "category":"influence"},
|
|
{"from":"k3", "to":"sheep_loss", "category":"influence"}
|
|
]
|
|
}
|
|
</textarea>
|
|
</div>
|
|
</div>
|
|
</body>
|
|
</html>
|