From 6787ac9e186e2544d74787fbb927087fc2585db2 Mon Sep 17 00:00:00 2001
From: ZitRo <zitros.lab@gmail.com>
Date: Mon, 11 Jan 2016 18:09:08 +0200
Subject: [PATCH] View save feature, xData describing KPI crash fix

---
 cache/projectTemplate.xml    |  52 +++++++++++++--
 package.json                 |   2 +-
 web/css/extras.css           |  10 ++-
 web/index.html               |  11 ++--
 web/js/CacheClassExplorer.js |  13 ++++
 web/js/ClassView.js          | 120 +++++++++++++++++++++++++++++++----
 web/js/Lib.js                |  12 +++-
 web/js/Logic.js              |   9 ++-
 web/js/Source.js             |  24 ++++++-
 9 files changed, 224 insertions(+), 29 deletions(-)

diff --git a/cache/projectTemplate.xml b/cache/projectTemplate.xml
index f3f2fd7..c41cb28 100644
--- a/cache/projectTemplate.xml
+++ b/cache/projectTemplate.xml
@@ -4,7 +4,7 @@
 <Description>
 Cache Class Explorer vX.X.X/*build.replace:pkg.version*/
 Class contains methods that return structured classes/packages data.</Description>
-<TimeChanged>63919,67431.456639</TimeChanged>
+<TimeChanged>63928,63957.580821</TimeChanged>
 <TimeCreated>63653,67019.989197</TimeCreated>
 
 <Method name="getAllNamespacesList">
@@ -193,7 +193,7 @@ Return structured data about class.</Description>
         set xd = classDefinition.XDatas.GetAt(i)
         for j=1:1:props.Properties.Count() {
             set pname = props.Properties.GetAt(j).Name
-            set:(pname '= "parent") $PROPERTY(oProp, pname) = $PROPERTY(xd, pname)
+            set:((pname '= "parent") && (pname '= "Object")) $PROPERTY(oProp, pname) = $PROPERTY(xd, pname)
         }
         do oXDatas.%DispatchSetProperty(xd.Name, oProp)
     }
@@ -319,7 +319,7 @@ Returns new (correct) super</Description>
 <Description>
 Setup basic output data object</Description>
 <ClassMethod>1</ClassMethod>
-<FormalSpec>packageName:%String</FormalSpec>
+<FormalSpec>packageName:%String,baseNamespace:%String,savedName:%String</FormalSpec>
 <Private>1</Private>
 <ReturnType>%ZEN.proxyObject</ReturnType>
 <Implementation><![CDATA[
@@ -327,6 +327,14 @@ Setup basic output data object</Description>
     set oData.basePackageName = packageName
     set oData.restrictPackage = 1 // expand classes only in base package
     set oData.classes = ##class(%ZEN.proxyObject).%New()
+
+    set ns = $namespace
+    zn baseNamespace
+    if $get(^ClassExplorer("savedView", ns_":"_savedName)) '= "" {
+        set oData.savedView = $get(^ClassExplorer("savedView", ns_":"_savedName))
+    }
+    zn ns
+
     quit oData
 ]]></Implementation>
 </Method>
@@ -348,9 +356,10 @@ Returns structured class data</Description>
 <FormalSpec>className:%String,namespace:%String</FormalSpec>
 <ReturnType>%ZEN.proxyObject</ReturnType>
 <Implementation><![CDATA[
+    set baseNamespace = $namespace
     zn:$GET(namespace)'="" namespace
     set package = $LISTTOSTRING($LIST($LISTFROMSTRING(className, "."), 1, *-1), ".")
-    set oData = ..getBaseOData(package)
+    set oData = ..getBaseOData(package, baseNamespace, "CLASS:"_className)
     do ..fillClassData(oData, className)
     quit oData
 ]]></Implementation>
@@ -363,8 +372,9 @@ Returns structured package data</Description>
 <FormalSpec>rootPackageName:%String,namespace:%String</FormalSpec>
 <ReturnType>%ZEN.proxyObject</ReturnType>
 <Implementation><![CDATA[
+    set baseNamespace = $namespace
     zn:$GET(namespace)'="" namespace
-    set oData = ..getBaseOData(rootPackageName)
+    set oData = ..getBaseOData(rootPackageName, baseNamespace, "PACKAGE:"_rootPackageName)
     set classes = ##class(%ResultSet).%New("%Dictionary.ClassDefinition:Summary")
     do classes.Execute()
     set listLen = $LISTLENGTH($LISTFROMSTRING(rootPackageName, ".")) // bottom level of package to extract
@@ -397,7 +407,7 @@ Returns structured package data</Description>
 <Description>
 REST interface for ClassExplorer</Description>
 <Super>%CSP.REST</Super>
-<TimeChanged>63697,73073.878177</TimeChanged>
+<TimeChanged>63928,63486.89174</TimeChanged>
 <TimeCreated>63648,30450.187229</TimeCreated>
 
 <XData name="UrlMap">
@@ -413,6 +423,8 @@ REST interface for ClassExplorer</Description>
    <Route Url="/GetAllNamespacesList" Method="GET" Call="GetAllNamespacesList"/>
    <Route Url="/GetPackageView" Method="GET" Call="GetPackageView"/>
    <Route Url="/GetMethod" Method="GET" Call="GetMethod"/>
+   <Route Url="/SaveView" Method="POST" Call="SaveView"/>
+   <Route Url="/ResetView" Method="GET" Call="ResetView"/>
 </Routes>
 ]]></Data>
 </XData>
@@ -441,6 +453,34 @@ Returns classTree by given class name</Description>
 ]]></Implementation>
 </Method>
 
+<Method name="SaveView">
+<Description>
+Saves the view preferences</Description>
+<ClassMethod>1</ClassMethod>
+<ReturnType>%Status</ReturnType>
+<Implementation><![CDATA[
+    set name = %request.Get("name")
+    set content = %request.Content.Read($$$MaxStringLength) // ~ 7mb
+    set ^test = name
+    set ^ClassExplorer("savedView", name) = content
+    write "{""OK"":true}"
+    return $$$OK
+]]></Implementation>
+</Method>
+
+<Method name="ResetView">
+<Description>
+Saves the view preferences</Description>
+<ClassMethod>1</ClassMethod>
+<ReturnType>%Status</ReturnType>
+<Implementation><![CDATA[
+    set name = %request.Get("name")
+    kill ^ClassExplorer("savedView", name)
+    write "{""OK"":true}"
+    return $$$OK
+]]></Implementation>
+</Method>
+
 <Method name="GetPackageView">
 <Description>
 Returns all package class trees by given package name</Description>
diff --git a/package.json b/package.json
index 3a3c65c..3c4a0b8 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "CacheClassExplorer",
-  "version": "1.12.0",
+  "version": "1.13.1",
   "description": "Class Explorer for InterSystems Caché",
   "directories": {
     "test": "test"
diff --git a/web/css/extras.css b/web/css/extras.css
index a5e7691..70641bb 100644
--- a/web/css/extras.css
+++ b/web/css/extras.css
@@ -63,12 +63,12 @@
 }
 
 .icon {
+    position: relative;
     display: inline-block;
     background-color: #333;
     border-radius: 12px;
     width: 24px;
     height: 24px;
-    position: relative;
     cursor: pointer;
     -webkit-transition: all .2s ease;
     -moz-transition: all .2s ease;
@@ -80,6 +80,14 @@
     user-select: none;
 }
 
+.icon img {
+    position: absolute;
+    width: 16px;
+    height: 16px;
+    left: 4px;
+    top: 4px;
+}
+
 .icon:hover {
     box-shadow: 0 0 5px 2px #ffcc1b;
 }
diff --git a/web/index.html b/web/index.html
index 1629bdb..22a66e8 100644
--- a/web/index.html
+++ b/web/index.html
@@ -60,10 +60,13 @@
                 <div class="inlineSearchBlock" id="diagramSearchBlock">
                     <input type="search" id="diagramSearch" placeholder="Search on diagram..."/>
                 </div>
-                <div id="button.diagramSearch" class="icon search"></div>
-                <div id="button.zoomIn" class="icon plus"></div>
-                <div id="button.zoomNormal" class="icon scaleNormal"></div>
-                <div id="button.zoomOut" class="icon minus"></div>
+                <div id="button.diagramSearch" class="icon search" title="Search"></div>
+                <div id="button.zoomIn" class="icon plus" title="Zoom In"></div>
+                <div id="button.zoomNormal" class="icon scaleNormal" title="Zoom Normal"></div>
+                <div id="button.zoomOut" class="icon minus" title="Zoom Out"></div>
+                <div id="button.saveView" class="icon pin" title="Keep Positions">
+                    <img id="saveViewIcon"/>
+                </div>
             </div>
             <div id="classView">
                 <div id="methodCodeView">
diff --git a/web/js/CacheClassExplorer.js b/web/js/CacheClassExplorer.js
index da4ad5a..bbd3639 100644
--- a/web/js/CacheClassExplorer.js
+++ b/web/js/CacheClassExplorer.js
@@ -22,6 +22,8 @@ var CacheClassExplorer = function (treeViewContainer, classViewContainer) {
         showSettingsButton: id("button.showSettings"),
         helpButton: id("button.showHelp"),
         infoButton: id("button.showInfo"),
+        saveViewButton: id("button.saveView"),
+        saveViewIcon: id("saveViewIcon"),
         methodCodeView: id("methodCodeView"),
         closeMethodCodeView: id("closeMethodCodeView"),
         methodLabel: id("methodLabel"),
@@ -258,4 +260,15 @@ CacheClassExplorer.prototype.init = function () {
 
     enableSVGDownload(this.classTree);
 
+    // default icon
+    this.elements.saveViewIcon.src = lib.image.pin;
+    this.elements.saveViewButton.addEventListener("click", function () {
+        self.classView.switchViewSave();
+        if (self.classView.viewSaving) {
+            self.classView.saveView();
+        } else {
+            self.source.resetView( self.NAMESPACE + ":" + self.classView.CURRENT_RENDER_NAME );
+        }
+    });
+
 };
\ No newline at end of file
diff --git a/web/js/ClassView.js b/web/js/ClassView.js
index 16f03db..970f1f8 100644
--- a/web/js/ClassView.js
+++ b/web/js/ClassView.js
@@ -25,6 +25,16 @@ var ClassView = function (parent, container) {
     this.HIGHLIGHTED_VIEW = null;
     this.SEARCH_INDEX = 0;
 
+    this.CURRENT_RENDER_NAME = "";
+
+    this.viewSaving = false;
+
+    /**
+     * Not to perform save too frequentry, this variable is used to control saving frequency.
+     * @type {number}
+     */
+    this.saveTimeout = 0;
+
     this.init();
 
 };
@@ -446,9 +456,10 @@ ClassView.prototype.getPropertyHoverText = function (prop, type) {
 /**
  * @param {string} name
  * @param classMetaData
+ * @param saved - Object with saved data.
  * @returns {joint.shapes.uml.Class}
  */
-ClassView.prototype.createClassInstance = function (name, classMetaData) {
+ClassView.prototype.createClassInstance = function (name, classMetaData, saved) {
 
     var classParams = classMetaData["parameters"],
         classProps = classMetaData["properties"],
@@ -458,7 +469,7 @@ ClassView.prototype.createClassInstance = function (name, classMetaData) {
         keyWordsArray = [name],
         self = this;
 
-    var classInstance = new joint.shapes.uml.Class({
+    var setup = {
         name: [{
             text: name,
             clickHandler: function () {
@@ -549,9 +560,18 @@ ClassView.prototype.createClassInstance = function (name, classMetaData) {
         classSigns: this.getClassSigns(classMetaData),
         classType: classMetaData.ClassType || "registered",
         SYMBOL_12_WIDTH: self.SYMBOL_12_WIDTH
+    };
+
+    if (saved && saved.position) setup.position = saved.position;
+
+    var classInstance = new joint.shapes.uml.Class(setup);
+
+    classInstance.on("change:position", function () {
+        self.prepareToSave();
     });
 
     classInstance.SEARCH_KEYWORDS = keyWordsArray.join(",").toLowerCase();
+    classInstance.NAME = name;
     this.objects.push(classInstance);
     this.graph.addCell(classInstance);
 
@@ -559,6 +579,15 @@ ClassView.prototype.createClassInstance = function (name, classMetaData) {
 
 };
 
+ClassView.prototype.prepareToSave = function () {
+
+    if (!this.viewSaving) return;
+
+    if (this.saveTimeout) clearTimeout(this.saveTimeout);
+    this.saveTimeout = setTimeout(this.saveView.bind(this), 700);
+
+};
+
 ClassView.prototype.showMethodCode = function (className, methodName) {
 
     var self = this;
@@ -701,6 +730,8 @@ ClassView.prototype.confirmRender = function (data) {
         uml = joint.shapes.uml, relFrom, relTo,
         classes = {}, connector;
 
+    this.switchViewSave(!!data.savedView);
+
     this.filterInherits(data);
 
     // Reset view and zoom again because it may cause visual damage to icons.
@@ -715,7 +746,11 @@ ClassView.prototype.confirmRender = function (data) {
 
     for (className in data["classes"]) {
         classes[className] = {
-            instance: this.createClassInstance(className, data["classes"][className])
+            instance: this.createClassInstance(
+                className,
+                data["classes"][className],
+                ((data.savedView || {}).classes || {})[className]
+            )
         };
     }
 
@@ -730,7 +765,11 @@ ClassView.prototype.confirmRender = function (data) {
                 relTo = (classes[pp] || {}).instance;
                 if (!relTo) {
                     classes[pp] = {
-                        instance: relTo = self.createClassInstance(pp, {})
+                        instance: relTo = self.createClassInstance(
+                            pp,
+                            {},
+                            ((data.savedView || {}).classes || {})[pp]
+                        )
                     };
                 }
                 if (relFrom && relTo) {
@@ -780,13 +819,15 @@ ClassView.prototype.confirmRender = function (data) {
     link("aggregation");
     link("association");
 
-    joint.layout.DirectedGraph.layout(this.graph, {
-        setLinkVertices: false,
-        nodeSep: 100,
-        rankSep: 100,
-        edgeSep: 20,
-        rankDir: data.layoutDirection || "TB"
-    });
+    if (!data.savedView) {
+        joint.layout.DirectedGraph.layout(this.graph, {
+            setLinkVertices: false,
+            nodeSep: 100,
+            rankSep: 100,
+            edgeSep: 20,
+            rankDir: data.layoutDirection || "TB"
+        });
+    }
 
     this.updateSizes();
 
@@ -794,16 +835,66 @@ ClassView.prototype.confirmRender = function (data) {
         this.paper.findViewByModel(this.links[i]).update();
     }
 
-    var bb = this.paper.getContentBBox(), q = this.paper;
+    var bb = this.paper.getContentBBox(),
+        q = this.paper;
+
     this.paper.setOrigin(
         q.options.width/2 - bb.width/2,
         q.options.height/2 - Math.min(q.options.height/2 - 100, bb.height/2)
     );
 
+    if (data.savedView) this.restoreView(data.savedView);
+
     this.onRendered();
 
 };
 
+ClassView.prototype.switchViewSave = function ( saving ) {
+
+    if (typeof saving === "undefined") saving = !this.viewSaving;
+    this.viewSaving = !!saving;
+    this.cacheClassExplorer.elements.saveViewIcon.src = lib.image["pin" + (saving ? "Active" : "")];
+
+};
+
+ClassView.prototype.saveView = function () {
+
+    if (!this.CURRENT_RENDER_NAME || !this.cacheClassExplorer.NAMESPACE) return;
+
+    var self = this,
+        name = this.cacheClassExplorer.NAMESPACE + ":" + this.CURRENT_RENDER_NAME;
+
+    var saved = {
+        classes: {},
+        zoom: this.PAPER_SCALE,
+        origin: {
+            x: Math.round(self.paper.options.origin.x),
+            y: Math.round(self.paper.options.origin.y)
+        }
+    };
+
+    this.graph.getElements().forEach(function (element) {
+        if (!element.NAME) return;
+        saved.classes[element.NAME] = {
+            position: element.attributes.position
+        }
+    });
+
+    this.cacheClassExplorer.source.saveView(name, saved);
+
+};
+
+ClassView.prototype.restoreView = function (data) {
+
+    // data.classes are parsed during class creation
+    if (data.zoom) { // do not swap with origin set
+        this.PAPER_SCALE = data.zoom;
+        this.zoom(0);
+    }
+    if (data.origin && data.origin.x && data.origin.y) this.paper.setOrigin(data.origin.x, data.origin.y);
+
+};
+
 ClassView.prototype.loadClass = function (className) {
 
     var self = this;
@@ -823,6 +914,7 @@ ClassView.prototype.loadClass = function (className) {
     });
 
     this.cacheClassExplorer.elements.className.textContent = className;
+    this.CURRENT_RENDER_NAME = "CLASS:" + className;
     this.cacheClassExplorer.updateURL();
 
 };
@@ -846,6 +938,7 @@ ClassView.prototype.loadPackage = function (packageName) {
     });
 
     this.cacheClassExplorer.elements.className.textContent = packageName;
+    this.CURRENT_RENDER_NAME = "PACKAGE:" + packageName;
     this.cacheClassExplorer.updateURL();
 
 };
@@ -881,6 +974,8 @@ ClassView.prototype.zoom = function (delta) {
         oy - (sh/2 - oy)*scaleDelta
     );
 
+    if (delta) this.prepareToSave(); // delta = null,0 when restore triggered
+
 };
 
 /**
@@ -1047,6 +1142,7 @@ ClassView.prototype.init = function () {
             self.paper.options.origin.y + e.pageY - relP.y
         );
         relP.x = e.pageX; relP.y = e.pageY;
+        self.prepareToSave();
     };
 
     this.cacheClassExplorer.elements.classViewContainer.addEventListener("mousemove", moveHandler);
diff --git a/web/js/Lib.js b/web/js/Lib.js
index 952e6f6..a0008c9 100644
--- a/web/js/Lib.js
+++ b/web/js/Lib.js
@@ -12,10 +12,16 @@ Lib.prototype.load = function (url, data, callback) {
     var xhr = new XMLHttpRequest();
 
     xhr.open(data ? "POST" : "GET", url);
+    if (typeof callback === "undefined") callback = function () {};
 
     xhr.onreadystatechange = function () {
         if (xhr.readyState === 4 && xhr.status === 200) {
-            return callback(null, JSON.parse(xhr.responseText) || {});
+            try {
+                return callback(null, JSON.parse(xhr.responseText) || {});
+            } catch (e) {
+                console.error(url, "Unable to parse:", { data: xhr.responseText });
+                return {};
+            }
         } else if (xhr.readyState === 4) {
             callback(xhr.responseText + ", " + xhr.status + ": " + xhr.statusText);
         }
@@ -312,5 +318,7 @@ Lib.prototype.image = {
     keyRed: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAACeklEQVQ4jX2TO0zTURTGv3vv/9F/H4Q2LZRHa4o8fASGGuJjMBEluDAQI0Tj4ICTkUQZ3YyJxsHEyRijgwZjdFBjbHQCnDAGMQqioDYgMUBttdACbf/3HheNoK1nPDnfL/m+cw7r7OxEqbKJdMlYd0QWeryrK00pzZiJu9wPdKVuC6IMAGilxHkiI6hrd08K6orOzkHEp5HK2o2j/pr2683NvVkhDgqiBC8mlkTwm+a5i5tqu1oFIKSN4WAYTy5cgrOjHWdfj0cF0R3JmK8oICeVp6O66kTQ5QQUAbaNkYataOs7g3zfKYSWs9iTTB/IcX6oqAWNs+2tlRU+ZNIAA6Bp2D35FgP9p1HzOY59UqElncVQwBstCmCM5XUh/jSEwN7UPFqvXoFOBmCUQXIGArSiFgpSjY8mU7Pg6yCMwzIMCMMAAIx4PVCMvSseoq75Mh8mJUgB/PcIAURgBRvDwQBeecvGTKXu/2NhhYv6tu+LsfaxF5G1hXk4wrUAAMU4Ug4Lg+EQbtXVzQuiY5xobgNglbGdO+y1x90zUwGLMTgmJoBPX3BtW9P7ly3VybRmuH4YxnNTyvOcKAGsO6Qc0f6IJh4eTibcTiXhFwwoKDzzeXHP5+/XQTGNCJZtb9wYANhERytM4+YRVTA90kbIMJAwLfUoUpeLVdeOWKRinKhYXOCSqNep6wM93nLTbRfQ6PEAysZgoOrNjc0NoTzn3aXEAMAVsGwB0At5RCsrwBjDkumEDgq7C7bkRN9KqgFoUqmP5Q5zqWNLUxlm4oCSuByqnx41Xcct+ZfhYgAOTOmaPvR0IbFrMZ3JZw23EWdameT8q/brZf9XPwERTf+roi2jYwAAAABJRU5ErkJggg==",
     keyGreen: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAACbElEQVQ4jX2TS0iUURzFz31933wz44yOjviYERTTIjCYkLJFkCW2cSGRUrRoYW1cuHAV7aJFm2odUYvCiFpUREOt1FZGWNEblUwTmxwdfI7OzHfvv8UgIc147upy7/n97zlwWVdXF4qJXFLQrAf1ud5cWbqZp6wZNe1/Qsrch6B1AJDFzCZLlqdKPgz2U/ef2BzmxSR0ym0qH6/pKL3d0ocNcRKCkrzgZE1wKuwrLdci3b5WwAgXwdE6XHxxHW3eTqQvf4hB0ANoFioIcDOmpKGz+kKwygsNgoaLmrF96G8fQG92ACa6BhxZOoEMP1UwgpBsf6S1MpTFCvI5JWbbPuHS0CAWa39AHNNgLSugkXCscAeMZYUSoO0tBNJHE4i33kRAWQjCBjQHCLJgBJ0znxPjqVkJ8Y8JDtuxoISV72msDDDsa0GA1DI0tf5NGxgwbF+h/GI5sNEqsHdl72Gbx/9FYGnRuNm+EJ/oeFPvbiUQ9tTmDwyHTDnwDEeh7jUkSNA5cJrbCdhkh9jBreeLPRNh7jD89HzBMmbgubX3e83bmiWxYvn4svWabH0VnJL5grcfmKHjdr14unk66YdXQ1UwMBjQq3KwR+FBpShOkkCOuzMuABiXzjqV1l3vmZy9UeLCG7WQTjom8Kwh48QjY+SYODihkDhp6rO8aqi2t9Q2/hxCTSUwcOEbrv7ov7MniizvKWbOAwzWuAO4KotIrBKMMchVL0hRHfldDU6LRd0ApNFmyim1Vw90Ngd+YRqARvBG4yQb9503juvuZgYAyTgmpFIjMy+Th38vrGe3NvwWm5YBpvk8ZP7L7qa/51LzXlKvOSsAAAAASUVORK5CYII=",
     minusSimple: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAL0lEQVQ4jWP8//8/IwMFgIkSzaMGUMkAFhiDkZHxHyka////z0QVFzCOJqThYAAAIOcKHde8N5QAAAAASUVORK5CYII=",
-    plusSimple: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAARUlEQVQ4jdWRMQ4AIAjErP9/M+eiiyEyYCTeSjiagiRaIj2zHBYABlgdwR8FrDdGsvZI6ncJ3OGkWte81EusLzhKfEIwANMfFhcD7xBqAAAAAElFTkSuQmCC"
+    plusSimple: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAARUlEQVQ4jdWRMQ4AIAjErP9/M+eiiyEyYCTeSjiagiRaIj2zHBYABlgdwR8FrDdGsvZI6ncJ3OGkWte81EusLzhKfEIwANMfFhcD7xBqAAAAAElFTkSuQmCC",
+    pin: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAQAAAC1+jfqAAADBmlDQ1BpY2MAAHjaY2BgnuDo4uTKJMDAUFBUUuQe5BgZERmlwH6egY2BmYGBgYGBITG5uMAxIMCHgYGBIS8/L5UBFTAyMHy7xsDIwMDAcFnX0cXJlYE0wJpcUFTCwMBwgIGBwSgltTiZgYHhCwMDQ3p5SUEJAwNjDAMDg0hSdkEJAwNjAQMDg0h2SJAzAwNjCwMDE09JakUJAwMDg3N+QWVRZnpGiYKhpaWlgmNKflKqQnBlcUlqbrGCZ15yflFBflFiSWoKAwMD1A4GBgYGXpf8EgX3xMw8BSMDVQYqg4jIKAUICxE+CDEESC4tKoMHJQODAIMCgwGDA0MAQyJDPcMChqMMbxjFGV0YSxlXMN5jEmMKYprAdIFZmDmSeSHzGxZLlg6WW6x6rK2s99gs2aaxfWMPZ9/NocTRxfGFM5HzApcj1xZuTe4FPFI8U3mFeCfxCfNN45fhXyygI7BD0FXwilCq0A/hXhEVkb2i4aJfxCaJG4lfkaiQlJM8JpUvLS19QqZMVl32llyfvIv8H4WtioVKekpvldeqFKiaqP5UO6jepRGqqaT5QeuA9iSdVF0rPUG9V/pHDBYY1hrFGNuayJsym740u2C+02KJ5QSrOutcmzjbQDtXe2sHY0cdJzVnJRcFV3k3BXdlD3VPXS8Tbxsfd99gvwT//ID6wIlBS4N3hVwMfRnOFCEXaRUVEV0RMzN2T9yDBLZE3aSw5IaUNak30zkyLDIzs+ZmX8xlz7PPryjYVPiuWLskq3RV2ZsK/cqSql01jLVedVPrHzbqNdU0n22VaytsP9op3VXUfbpXta+x/+5Em0mzJ/+dGj/t8AyNmf2zvs9JmHt6vvmCpYtEFrcu+bYsc/m9lSGrTq9xWbtvveWGbZtMNm/ZarJt+w6rnft3u+45uy9s/4ODOYd+Hmk/Jn58xUnrU+fOJJ/9dX7SRe1LR68kXv13fc5Nm1t379TfU75/4mHeY7En+59lvhB5efB1/lv5dxc+NH0y/fzq64Lv4T8Ffp360/rP8f9/AA0ADzTzG2NJAAAAIGNIUk0AAHolAACAgwAA+f8AAIDpAAB1MAAA6mAAADqYAAAXb5JfxUYAAAACYktHRAD/h4/MvwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB+ABCw0JBgCIkrwAAADNSURBVCjPdZA9DgFRFIVPMCFRSFQTBQ293z1oRWMJLECoLUHsAdEoRERjA0QvEhGNaBUa8inGzDwznFu83HvO/XlHKBQ5ZmTcTD8EHWCH7WQRhfGQVNZCtqQfEywmONhih1dYjPGxJetTcUTU63Yxj0lKqa2G0jooqrp3yVQbobUQBe4EMSZmfrPKNUBbQR9KHD166XabPux18La/9DRc+SiHwIozACcS/gTn6QIjRJErsCPyLWgCfdxzb/RM60SFCy2jlCf5LRhQQ//jDXleGJr4KuKfAAAAJXRFWHRkYXRlOmNyZWF0ZQAyMDE2LTAxLTExVDEzOjA5OjA2KzAxOjAw8Rgr6AAAACV0RVh0ZGF0ZTptb2RpZnkAMjAxNi0wMS0xMVQxMzowOTowNiswMTowMIBFk1QAAAA3dEVYdGljYzpjb3B5cmlnaHQAQ29weXJpZ2h0IDE5OTkgQWRvYmUgU3lzdGVtcyBJbmNvcnBvcmF0ZWQxbP9tAAAAHHRFWHRpY2M6ZGVzY3JpcHRpb24ARG90IEdhaW4gMjAlk5c01gAAAB10RVh0aWNjOm1hbnVmYWN0dXJlcgBEb3QgR2FpbiAyMCWy7qr9AAAAFnRFWHRpY2M6bW9kZWwARG90IEdhaW4gMjAlMnX1qwAAAABJRU5ErkJggg==",
+    pinActive: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JQAAgIMAAPn/AACA6QAAdTAAAOpgAAA6mAAAF2+SX8VGAAAAe1BMVEX/////zBv/zBv/zBv/zBv/zBv/zBv/zBv/zBv/zBv/zBv/zBv/zBv/zBv/zBv/zBv/zBv/zBv/zBv/zBv/zBv/zBv/zBv/zBv/zBv/zBv/zBv/zBv/zBv/zBv/zBv/zBv/zBv/zBv/zBv/zBv/zBv/zBv/zBv/zBv////a7dsHAAAAJ3RSTlMAQPLg8wreGfQY6utB+GxtU/taZ7ywq+3urK+9aL6trpeZf4BeYC49Q3tiAAAAAWJLR0QAiAUdSAAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB+ABCw0HIEwGOs8AAABlSURBVBjTXc1HEoAwCEBRYu+99879b2hGo0b+KrzJAACPKSpPY/Ckq8gz9BfAtBAtG6QcRFeewaVAf3g+ov8dgQCvwmeO4hviRECa5UVZ1U3bSVvaHv4NI4FpJrCsBLadwHGIxwkpXgeBl9dpZwAAACV0RVh0ZGF0ZTpjcmVhdGUAMjAxNi0wMS0xMVQxMzowNzozMyswMTowMDNmMx8AAAAldEVYdGRhdGU6bW9kaWZ5ADIwMTYtMDEtMTFUMTM6MDc6MzIrMDE6MDDkTIAXAAAAAElFTkSuQmCC"
 };
\ No newline at end of file
diff --git a/web/js/Logic.js b/web/js/Logic.js
index 12a3704..1d43d22 100644
--- a/web/js/Logic.js
+++ b/web/js/Logic.js
@@ -7,7 +7,7 @@ var Logic = function (parent) {
 /**
  * Modify data, add relations, connections, helpers.
  *
- * @param {{basePackageName: string, classes: object<string,*>, restrictPackage: number}} data
+ * @param {*} data
  */
 Logic.prototype.process = function (data) {
 
@@ -16,6 +16,13 @@ Logic.prototype.process = function (data) {
 
     this.data = data;
 
+    if (data.savedView) try {
+        data.savedView = JSON.parse(data.savedView);
+    } catch (e) {
+        delete data.savedView;
+        console.log("! Unable to deserialize savedView.");
+    }
+
     data.classes["%Persistent"] = data.classes["%Library.Persistent"] = {
         $classType: "Persistent"
     };
diff --git a/web/js/Source.js b/web/js/Source.js
index 1585aef..41085c2 100644
--- a/web/js/Source.js
+++ b/web/js/Source.js
@@ -45,7 +45,8 @@ Source.prototype.getMethod = function (className, methodName, callback) {
             + encodeURIComponent(methodName)
             + (this.cue.NAMESPACE ? "&namespace=" + encodeURIComponent(this.cue.NAMESPACE) : ""),
         null,
-        callback);
+        callback
+    );
 
 };
 
@@ -60,7 +61,26 @@ Source.prototype.getClassView = function (className, callback) {
         this.URL + "/GetClassView?name=" + encodeURIComponent(className)
             + (this.cue.NAMESPACE ? "&namespace=" + encodeURIComponent(this.cue.NAMESPACE) : ""),
         null,
-        callback);
+        callback
+    );
+
+};
+
+Source.prototype.saveView = function (packageName, data) {
+
+    lib.load(
+        this.URL + "/SaveView?name=" + encodeURIComponent(packageName),
+        data,
+        function (e) { console.log("View saved."); }
+    );
+
+};
+
+Source.prototype.resetView = function (packageName) {
+
+    lib.load(
+        this.URL + "/ResetView?name=" + encodeURIComponent(packageName)
+    );
 
 };