Browse Source

Frontend, backend, ops changes

* Added ops to test convolution
* Removed output button, now runs on selection multiple nodes which
  fetches data
* Added image visualizer if data startsWith "data:image"
luis 7 years ago
parent
commit
07865e88c2

+ 14 - 0
browser/vue-flow/src/assets/default-theme.css

@@ -107,6 +107,13 @@ button:active::after,
   opacity: 0.2;
 }
 
+.content-input {
+  word-wrap: break-word;
+  overflow: hidden;
+}
+
+.content-input,
+textarea,
 input {
   padding: 20px;
   outline: none;
@@ -114,6 +121,13 @@ input {
   transition: all var(--transition-speed);
 }
 
+/* only resize textarea vertical */
+textarea {
+  display: block;
+  resize: vertical;
+  height: min-content;
+}
+
 input::placeholder {
   opacity: 0.4;
 }

+ 1 - 1
browser/vue-flow/src/assets/doc/appinfo.md

@@ -15,7 +15,7 @@
 * **Move Node**: Mouse click and drag
 * **Links**: Press `shift` and Drag from a node/socket to a socket highlighted in green
 * **Links(alternative)**: Toggle socket visualisation in the panel and Drag from a socket to a socket highlighted in green
-* **Remove Link**: Simple click on the link when it turns red
+* **Remove Link**: Middle button click on the link
 * **Create a portal**: Drag an output socket from a node to an empty space check <a href="readme#portal-from" target="_blank">portals</a> on readme for further information
 
 <small>&copy; Luis Figueiredo (luisf@hexasoftware.com)</small>

+ 22 - 4
browser/vue-flow/src/assets/doc/readme.md

@@ -164,13 +164,16 @@ Same as f.SetVar passing the variable name as a property of the node
 
 ### Output
 
+_Deprecated_  
+See [here](#actions)
+
 > Flow-UI only
 
-Output is a special node that indicates the overall flow output,
+~~Output is a special node that indicates the overall flow output,
 the UI contains a RUN button that will be visible if there is an output node and
-the flow will be executed from the output node.
+the flow will be executed from the output node.~~
 
-There can be only one output node
+~~There can be only one output node~~
 
 ### Portal From
 
@@ -185,7 +188,7 @@ node
 
 > WIP
 
-## Ideas
+## Ideas/TODOS
 
 ### Matching types
 
@@ -246,6 +249,15 @@ would be good to have the same by right clicking in other subjects:
   * toggle Show Triggers
   * Reset view
 
+### Actions
+
+> TODO
+
+Instead of having a single Output node that shows the RUN button we would setup
+nodes to be processed in an action having possible multiple nodes per action
+
+`something like inspecting node and defining a list of actions that node would be processed such as [TRAIN, MAIN]`
+
 ## TODO
 
 ### Readme
@@ -293,6 +305,12 @@ would be good to have the same by right clicking in other subjects:
 
 ## Changelog
 
+25/02/2018
+
+* **UI/UX**: Added image visualization if the content result is a dataurl
+* **UI/UX**: Removed flow-ui Output node source, the button `run` which is now
+  Process will process and fetch data from selected nodes
+
 18/02/2018
 
 * **frontend**: Refactored nodes and links to improve performance with lots of elements

+ 5 - 3
browser/vue-flow/src/assets/lines-theme.css

@@ -21,9 +21,9 @@
   --selector-color: var(--primary);
   --node-selection: rgba(0, 187, 238, 0.7);
 
-  /*--transition-speed: 0.3s;*/
+  /* --transition-speed: 0.3s; */
 
-  /*--transition-speed-slow: 0.7s;*/
+  /* --transition-speed-slow: 0.7s; */
 }
 
 .lines .app-header {
@@ -40,9 +40,11 @@
   background: transparent !important;
 }
 
+.lines .content-input,
+.lines textarea,
 .lines input {
   border: solid 1px var(--border-color);
-  box-shadow: none; /*0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24);*/
+  box-shadow: none; /* 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24);*/
   color: #777;
 }
 

+ 6 - 0
browser/vue-flow/src/assets/style.css

@@ -83,7 +83,13 @@ small {
   color: #555;
 }
 
+em {
+  font-style: italic;
+}
+
+.content-input,
 input {
+  font-size: 12px;
   outline: none;
   background: #fff;
   width: 100%;

+ 4 - 3
browser/vue-flow/src/components/app-flow.vue

@@ -46,6 +46,7 @@
               @nodeInspect="nodeInspectStart(...arguments)"
               @nodeProcess="nodeProcess(...arguments)"
               @nodeDoubleClick="nodeInspectStart(...arguments,true)"
+              @nodeViewData="modalData=true"
               @activityPointerDown="nodeInspectStart($event,true,true)"
               @documentSave="documentSave"
 
@@ -146,13 +147,13 @@ export default {
         this.panel = 'inspector'
       }
       if (this.panel !== 'inspector') {
-        return
+
       }
 
       // this.nodeInspect = node
 
       // if (!changePane) { return }
-      this.$nextTick(() => {
+      /* this.$nextTick(() => {
         // panel input
         if (!this.$refs.inspector) { }
         const insp = this.$refs.inspector
@@ -167,7 +168,7 @@ export default {
         }
         targetInput.setSelectionRange(0, targetInput.value.length)
         targetInput.focus()
-      })
+      }) */
     },
     funcsSizeUpdate (ev, size) {
       this.funcsSize = size

+ 15 - 6
browser/vue-flow/src/components/flow/editor.js

@@ -7,7 +7,6 @@ import FlowModalData from './modal-data' // NEW 15/02/2018
 import HxContextMenu from '@/components/shared/hx-contextmenu'
 import SvgDefs from './svgdefswrapper'
 import utils from '@/utils/utils'
-import _debounce from 'lodash.debounce'
 
 export default {
   name: 'FlowManager',
@@ -64,6 +63,9 @@ export default {
         }
       }
       return highlight
+    },
+    selectionCount () {
+      return Object.keys(this.nodeSelection).length
     }
   },
   mounted () {
@@ -79,7 +81,7 @@ export default {
       'NOTIFICATION_ADD',
       'DOCUMENT_SYNC',
       'NODE_RAISE', 'NODE_UPDATE', 'NODE_ADD', 'NODE_REMOVE', 'NODE_INSPECT', 'NODE_PROCESS', 'NODE_TRAIN',
-      'NODE_SELECTION_ADD', 'NODE_SELECTION_CLEAR',
+      'NODE_SELECTION_ADD', 'NODE_SELECTION_SET', 'NODE_SELECTION_CLEAR',
       'LINK_ADD', 'LINK_REMOVE',
       'TRIGGER_ADD', 'TRIGGER_REMOVE' ]),
 
@@ -445,7 +447,16 @@ export default {
       }
       this.NODE_ADD([newNode])
     },
-
+    nodeProcess (nodeId) {
+      const n = this.nodeById(nodeId)
+      this.nodeInspect(n.id, true)
+      this.NODE_PROCESS(n.id)
+      // this.NODE_SELECTION_SET([n])
+      // this.nodeSelectionProcess()
+    },
+    nodeSelectionProcess () {
+      this.NODE_PROCESS(Object.keys(this.nodeSelection))
+    },
     managerDrop (ev) {
       ev.preventDefault()
       const reg = ev.dataTransfer.getData('text')
@@ -496,12 +507,10 @@ export default {
         }
       })
     },
-
     documentProcess () {
       const n = this.nodeData.nodes.find(n => n.src === 'Output')
-      this.NODE_PROCESS(n.id)
+      this.nodeProcess(n.id)
     },
-
     createPortal (nodeID, x, y) {
       // Find nodeID
       let node = this.nodeData.nodes.find(n => n.id === nodeID)

+ 21 - 13
browser/vue-flow/src/components/flow/editor.vue

@@ -42,7 +42,7 @@
           @socketPointerDown="socketPointerDown(n.id,...arguments)"
           @triggerPointerDown="triggerPointerDown(n.id,...arguments)"
           @nodeDoubleClick="$emit('nodeDoubleClick',n.id)"
-          @nodeRightClick="$refs.menu.open($event,n.id)"
+          @nodeRightClick="$refs.menu.open($event,n)"
           @activityPointerDown="$emit('activityPointerDown',n.id)"
         />
 
@@ -71,26 +71,37 @@
       <button v-if="panzoom.x!=0 || panzoom.y!=0 || panzoom.zoom!=1" @click="panzoomReset">
         Reset view
       </button>
-      <button v-if="outputNode" @click="documentProcess" class="primary-inverse">RUN</button>
+      <button v-if="selectionCount>0" @click="nodeSelectionProcess" class="primary-inverse">Process</button>
     </div>
     <div class="flow-container__info">
       x:{{ panzoom.x.toFixed(2) }} y:{{ panzoom.y.toFixed(2) }} scale:{{ panzoom.zoom.toFixed(2) }} |
       {{ nodeData.nodes.length }} nodes
       {{ nodeData.links.length }} links
       {{ nodeData.triggers.length }} triggers
-      {{ Object.keys(nodeSelection).length }} selected
+      {{ selectionCount }} selected
     </div>
     <hx-context-menu ref="menu">
       <template slot-scope="d" >
         <div class="flow-node__context-menu">
-          <div class="hover" @click="NODE_PROCESS(d.userData)">Run</div>
-          <div class="hover" @click="createPortal(d.userData)">Create Portal</div>
-          <hr>
-          <div class="hover" @click="nodeRemove(d.userData)">Delete</div>
+
+          <!-- for the selection -->
+          <div v-if="d.userData && nodeSelection[d.userData.id]">
+            <div class="hover item" @click="nodeSelectionProcess">Run ({{ selectionCount }})</div>
+            <div class="hover item" @click="$emit('nodeViewData')">View ({{ selectionCount }})</div>
+            <hr>
+            <div class="hover item" @click="NODE_REMOVE(nodeSelection)">Delete ({{ selectionCount }})</div>
+          </div>
+          <!-- for the context -->
+          <div v-else>
+            <div class="hover item" @click="nodeProcess(d.userData.id)">Run</div>
+            <hr>
+            <div class="hover item" @click="nodeRemove(d.userData.id)">Delete</div>
+          </div>
+
           <hr>
-          <div class="hover" @click="NODE_TRAIN(d.userData)">Train(temporary)</div>
+          <div class="hover item" @click="NODE_TRAIN(d.userData.id)">Train(temporary)</div>
           <hr>
-          <div class="hover" @click="nodeInspect(d.userData,true)">Inspect</div>
+          <div class="hover item" @click="nodeInspect(d.userData.id,true)">Inspect</div>
         </div>
       </template>
     </hx-context-menu>
@@ -164,12 +175,9 @@
   width:180px;
 }
 
-.flow-node__context-menu > div {
+.flow-node__context-menu .item {
   padding:10px 20px;
   cursor: pointer;
-}
-
-.flow-node__context-menu > * {
   width:100%;
 }
 

+ 26 - 6
browser/vue-flow/src/components/flow/modal-data.vue

@@ -1,19 +1,28 @@
 <template>
   <div slot="body">
+    Multiple node visualizer
     WIP: canvas maybe?
     <hr>
-    Data:
-    <img
-      v-if="type == 'image'"
-      :src="data">
-    <p v-else>{{ data }}</p>
+    <div class="flow-modal-data__container">
+      <div
+        class="flow-modal-data__item"
+        v-for="(n,k) of nodeSelection"
+        :key="'data'+k"
+      >
+        Node: [{{ n.id }}] - {{ n.label }}
+        <img
+          v-if="activity.nodes[n.id] && activity.nodes[n.id].data && activity.nodes[n.id].data.toString().startsWith('data:image')"
+          :src="activity.nodes[n.id].data">
+        <p v-else>{{ activity.nodes[n.id] && activity.nodes[n.id].data }}</p>
+      </div>
+    </div>
   </div>
 </template>
 <script>
 import {mapGetters} from 'vuex'
 export default {
   computed: {
-    ...mapGetters('flow', ['activity', 'nodeInspect']),
+    ...mapGetters('flow', ['activity', 'nodeInspect', 'nodeSelection']),
     nodeActivity () {
       return this.activity && this.activity.nodes && this.activity.nodes[this.nodeInspect.id]
     },
@@ -37,4 +46,15 @@ export default {
 }
 </script>
 <style>
+.flow-modal-data__container {
+  display:flex;
+  flex-flow:row;
+  flex-wrap: wrap;
+}
+
+.flow-modal-data__item {
+  display:flex;
+  flex-flow:column;
+  padding:10px;
+}
 </style>

+ 2 - 1
browser/vue-flow/src/components/flow/node.vue

@@ -188,8 +188,9 @@ export default {
       return this.activity && this.activity.nodes && this.activity.nodes[this.node.id]
     },
     color () {
-      return this.node.color ||
+      const ret = this.node.color ||
         (this.registry[this.node.src].style && this.registry[this.node.src].style.color)
+      return ret
     },
     style () {
       return this.registry[this.node.src].style || {}

+ 22 - 8
browser/vue-flow/src/components/flow/panel-inspector.vue

@@ -43,11 +43,19 @@
             v-if="nodeActivity.data"
             class="flow-inspector--properties-result">
             <label>Result</label>
-            <div class="property">
+
+            <div
+              v-if="result.startsWith('data:image')"
+              class="property">
+              <img style="width:100%;height:auto" :src="result">
+            </div>
+            <div
+              v-else
+              class="property">
               {{ result.substr(0,100) }}
               {{ result.length > 100 ? '...':'' }}
             </div>
-            <button @click="$emit('dataClick')">Visualize</button>
+            <button @click="NODE_SELECTION_SET([nodeInspect]);$emit('dataClick')">Visualize</button>
           </div>
           <div
             v-if="nodeActivity.error"
@@ -76,14 +84,19 @@
           <div
             class="flow-inspector__param"
             v-for="(n,i) in registry[nodeInspect.src].inputs"
-            :key="i">
+            :key="nodeInspect.id +'-prop-'+i">
             <label>{{ n.name }}:{{ n.type }}</label>
-            <input
+            <hx-el-editable
+              ref="inputs"
+              v-model="nodeInspect.defaultInputs[i]"
+              @input="localChange"
+            />
+            <!--<textarea
               ref="inputs"
               type="text"
               v-model="nodeInspect.defaultInputs[i]"
               @input="localChange"
-            >
+              />-->
           </div>
         </div>
 
@@ -92,7 +105,7 @@
       <div class="flow-inspector__area flow-inspector--control">
         <button
           class="primary-inverse"
-          @click="NODE_PROCESS(nodeInspect.id)">Run</button>
+          @click="NODE_PROCESS([nodeInspect.id])">Run</button>
       </div>
     </template>
     <template v-else>
@@ -106,10 +119,11 @@
 import {mapGetters, mapActions} from 'vuex'
 import FlowPanzoom from '@/components/flow/panzoom'
 import FlowNode from '@/components/flow/node'
+import HxElEditable from '@/components/shared/hx-eleditable'
 
 export default {
   name: 'FlowInspector',
-  components: {FlowPanzoom, FlowNode},
+  components: {FlowPanzoom, FlowNode, HxElEditable},
   data () {
     return {
       nodeInspect: null
@@ -130,7 +144,7 @@ export default {
     }
   },
   methods: {
-    ...mapActions('flow', ['NODE_UPDATE', 'DOCUMENT_SYNC', 'NODE_PROCESS']),
+    ...mapActions('flow', ['NODE_UPDATE', 'DOCUMENT_SYNC', 'NODE_PROCESS', 'NODE_SELECTION_SET']),
     localChange () {
       let nodeUpdates = [JSON.parse(JSON.stringify(this.nodeInspect))]
 

+ 56 - 0
browser/vue-flow/src/components/shared/hx-eleditable.vue

@@ -0,0 +1,56 @@
+Content editable wrapper
+<template>
+  <pre
+    tabindex="1"
+    class="content-input"
+    contentEditable="true"
+    v-html="local"
+    @focus="focus"
+    @input="update"
+    @keydown.stop
+  />
+</template>
+<script>
+export default {
+  props: {
+    'value': {type: String, default: ''}
+  },
+  data () {
+    return {
+      local: ''
+    }
+  },
+  watch: {
+    value (val, oldVal) {
+      if (val === this.$el.innerText) { return }
+      this.local = val
+    }
+  },
+  mounted () {
+    this.local = this.value
+  },
+  methods: {
+    focus (ev) {
+      if (document.body.createTextRange) {
+        const range = document.body.createTextRange()
+        range.moveToElementText(this.$el)
+        range.select()
+      } else if (window.getSelection) {
+        const selection = window.getSelection()
+        const range = document.createRange()
+        range.selectNodeContents(this.$el)
+        selection.removeAllRanges()
+        selection.addRange(range)
+      }
+      // console.log('Focusing')
+      // document.execCommand('selectAll', false, null)
+    },
+    update (ev) {
+      this.$emit('input', this.$el.innerText)
+    }
+  }
+
+}
+</script>
+<style>
+</style>

+ 6 - 2
browser/vue-flow/src/store/flow/actions.js

@@ -33,12 +33,16 @@ export default {
   [m.NODE_INSPECT] ({commit}, nodeId) {
     commit(m.NODE_INSPECT, nodeId)
   },
-  [m.NODE_PROCESS] (ctx, nodeId) {
-    flowService.nodeProcess(nodeId)
+  [m.NODE_PROCESS] (ctx, nodeIds) {
+    // Should be an array now
+    flowService.nodeProcess(nodeIds)
   },
   [m.NODE_SELECTION_CLEAR] (ctx) {
     ctx.commit(m.NODE_SELECTION_CLEAR)
   },
+  [m.NODE_SELECTION_SET] (ctx, nodes) {
+    ctx.commit(m.NODE_SELECTION_SET, nodes)
+  },
   [m.NODE_SELECTION_REMOVE] (ctx, nodes) {
     ctx.commit(m.NODE_SELECTION_REMOVED, nodes)
   },

+ 0 - 5
browser/vue-flow/src/store/flow/default-registry.js

@@ -5,11 +5,6 @@ export default{
     style: { color: '#686', shape: 'circle' },
     props: {'input': ''} // should be sent in the node
   },
-  'Output': {
-    categories: ['core'],
-    inputs: [{type: 'interface {}'}],
-    style: { color: '#111', shape: 'circle' }
-  },
   'Var': {
     categories: ['core'],
     inputs: [{type: 'interface {}', name: 'initial'}],

+ 1 - 1
browser/vue-flow/src/store/flow/mutation-types.js

@@ -4,7 +4,7 @@ var actions = [
   'DOCUMENT_UPDATE', 'DOCUMENT_SYNC',
   'ACTIVITY_UPDATE',
   'NODE_RAISE', 'NODE_UPDATE', 'NODE_ADD', 'NODE_REMOVE', 'NODE_INSPECT', 'NODE_PROCESS', 'NODE_TRAIN',
-  'NODE_SELECTION_CLEAR', 'NODE_SELECTION_REMOVE', 'NODE_SELECTION_ADD',
+  'NODE_SELECTION_CLEAR', 'NODE_SELECTION_REMOVE', 'NODE_SELECTION_SET', 'NODE_SELECTION_ADD',
   'LINK_ADD', 'LINK_REMOVE',
   'TRIGGER_ADD', 'TRIGGER_REMOVE',
   'NOTIFICATION_ADD', 'NOTIFICATION_CLEAR'

+ 7 - 0
browser/vue-flow/src/store/flow/mutations.js

@@ -106,6 +106,13 @@ export default {
       Vue.delete(state.nodeSelection, n.id)
     }
   },
+  [m.NODE_SELECTION_SET] (state, nodes) {
+    state.nodeSelection = {}
+    for (let k in nodes) {
+      const n = nodes[k]
+      Vue.set(state.nodeSelection, n.id, n)
+    }
+  },
   [m.NODE_SELECTION_ADD] (state, nodes) {
     for (let k in nodes) {
       const n = nodes[k]

+ 2 - 0
go/src/demos/cmd/demo1/main.go

@@ -2,6 +2,7 @@ package main
 
 import (
 	"demos/cmd/demo1/assets"
+	"demos/ops/decodeops"
 	"demos/ops/defaultops"
 	"demos/ops/devops"
 	"demos/ops/genericops"
@@ -47,6 +48,7 @@ func main() {
 	mlReg.Merge(genericops.New())
 	mlReg.Merge(stringops.New())
 	mlReg.Merge(webops.New())
+	mlReg.Merge(decodeops.New())
 
 	mux.Handle("/machinelearning/", c.Build(
 		http.StripPrefix("/machinelearning", flowserver.New(mlReg, "ml")),

+ 44 - 0
go/src/demos/ops/decodeops/decodeops.go

@@ -0,0 +1,44 @@
+package decodeops
+
+import (
+	"bytes"
+	"flow/registry"
+	"image"
+	// Image decoders
+	_ "image/gif"
+	_ "image/jpeg"
+	_ "image/png"
+)
+
+//New decoding ops
+func New() *registry.R {
+	r := registry.New()
+	r.Add(DecodeImage).Tags("experiment-decode")
+	return r
+}
+
+// DecodeImage from a byte array
+func DecodeImage(in []byte) (image.Image, error) {
+	br := bytes.NewReader(in)
+	im, _, err := image.Decode(br)
+	return im, err
+}
+
+/*func decodePNG(in []byte) (image.Image, error) {
+	br := bytes.NewReader(in)
+	im, err := png.Decode(br)
+	if err != nil {
+		return nil, err
+	}
+	return im, nil
+}
+
+func decodeJPG(in []byte) (image.Image, error) {
+	br := bytes.NewReader(in)
+	im, err := jpeg.Decode(br)
+	if err != nil {
+		return nil, err
+	}
+
+	return im, nil
+}*/

+ 115 - 2
go/src/demos/ops/ml/experiments.go

@@ -3,13 +3,73 @@ package ml
 import (
 	"bytes"
 	"encoding/base64"
+	"errors"
 	"flow/flowserver"
 	"image"
 	"image/png"
+	"log"
+	"math"
+
+	"gonum.org/v1/gonum/mat"
 )
 
+// imageToMat create a grayscaled matrix of the image
+func imageToGrayMatrix(im image.Image) (mat.Matrix, error) {
+	dims := im.Bounds().Size()
+	fdata := make([]float64, dims.X*dims.Y)
+
+	for y := 0; y < dims.Y; y++ {
+		for x := 0; x < dims.X; x++ {
+			oldPixel := im.At(x, y)
+			r, g, b, _ := oldPixel.RGBA()
+			lum := (19595*r + 38470*g + 7471*b + 1<<15) >> 24
+			fdata[x+y*dims.X] = float64(lum) / 256
+		}
+	}
+	m := mat.NewDense(dims.Y, dims.X, fdata)
+	return m, nil
+}
+
+func displayImg(img image.Image) (flowserver.Base64Data, error) {
+	pngEncoded := bytes.NewBuffer(nil)
+	err := png.Encode(pngEncoded, img)
+	if err != nil {
+		return flowserver.Base64Data([]byte{}), err
+	}
+	base64enc := base64.StdEncoding.EncodeToString(pngEncoded.Bytes())
+	out := bytes.NewBuffer(nil)
+	out.WriteString("data:image/png;base64,")
+	out.WriteString(base64enc)
+
+	return flowserver.Base64Data(out.String()), nil
+
+}
+func displayGrayMat(m mat.Matrix) (flowserver.Base64Data, error) {
+	r, c := m.Dims()
+
+	img := image.NewGray(image.Rect(0, 0, c, r))
+	for y := 0; y < r; y++ {
+		for x := 0; x < c; x++ {
+			img.Pix[x+y*c] = byte(m.At(y, x) * 255)
+		}
+	}
+	pngEncoded := bytes.NewBuffer(nil)
+
+	err := png.Encode(pngEncoded, img)
+	if err != nil {
+		return flowserver.Base64Data([]byte{}), err
+	}
+	base64enc := base64.StdEncoding.EncodeToString(pngEncoded.Bytes())
+	out := bytes.NewBuffer(nil)
+	out.WriteString("data:image/png;base64,")
+	out.WriteString(base64enc)
+
+	return flowserver.Base64Data(out.String()), nil
+
+}
+
 // Test
-func toGrayImage(data []float64, w, h int) (flowserver.Base64Data, error) {
+func toGrayImage(data []byte, w, h int) (flowserver.Base64Data, error) {
 	// Convert matrix to byte 0-255
 
 	/*bdata := make([]byte, w*h)
@@ -19,7 +79,7 @@ func toGrayImage(data []float64, w, h int) (flowserver.Base64Data, error) {
 
 	img := image.NewGray(image.Rect(0, 0, w, h))
 	for i, v := range data {
-		img.Pix[i] = byte(v * 255)
+		img.Pix[i] = v
 	}
 	pngEncoded := bytes.NewBuffer(nil)
 
@@ -41,5 +101,58 @@ func toGrayImage(data []float64, w, h int) (flowserver.Base64Data, error) {
 			bdata[x + y *w] = data[
 		}
 	}*/
+}
+
+// Convolution matrix
+func matConv(a mat.Matrix, conv mat.Matrix) (mat.Matrix, error) {
+	convR, convC := conv.Dims()
+
+	if convR&1 == 0 || convC&1 == 0 {
+		return nil, errors.New("kernel matrix should have odd columns and odd rows")
+	}
+	midR := int(math.Floor(float64(convR) / 2))
+	midC := int(math.Floor(float64(convC) / 2))
+	log.Println("Middle:", midR, midC)
+
+	norm := float64(0)
+	for cy := 0; cy < convR; cy++ {
+		for cx := 0; cx < convC; cx++ {
+			norm += conv.At(cy, cx)
+		}
+	}
+	if norm == 0 {
+		norm = 1.0
+	}
+
+	rows, cols := a.Dims()
+	ret := mat.NewDense(rows, cols, nil)
+	for y := 0; y < rows; y++ { // Matrix loop
+		for x := 0; x < cols; x++ {
+			acc := float64(0) //accumulator
+			for cy := 0; cy < convR; cy++ {
+				matY := y + cy - midR
+				if matY < 0 || matY >= rows {
+					continue
+				}
+				for cx := 0; cx < convC; cx++ {
+					matX := x + (cx - midC)
+					if matX < 0 || matX >= cols {
+						continue
+					}
+					acc += a.At(matY, matX) * (conv.At(cy, cx) / norm)
+				}
+			}
+			//acc /= 9
+			if acc > 1.0 {
+				acc = 1
+			}
+			if acc < 0.0 {
+				acc = 0.0
+			}
+
+			ret.Set(y, x, acc)
+		}
+	}
+	return ret, nil
 
 }

+ 7 - 1
go/src/demos/ops/ml/gonumops.go

@@ -39,7 +39,13 @@ func New() *registry.R {
 		Tags("gonum").
 		Extra("style", registry.M{"color": "#953"})
 
-	r.Add(toGrayImage).Tags("experiment")
+	registry.Describer(
+		r.Add(imageToGrayMatrix),
+		r.Add(displayImg),
+		r.Add(toGrayImage),
+		r.Add(matConv).Inputs("matrix", "conv"),
+		r.Add(displayGrayMat),
+	).Tags("experiment")
 
 	return r
 }

+ 4 - 5
go/src/demos/ops/webops/webops.go

@@ -16,19 +16,18 @@ func New() *registry.R {
 	return r
 }
 
-func httpGet(url string) (string, error) {
+func httpGet(url string) ([]byte, error) {
 
 	res, err := http.Get(url)
 	if err != nil {
-		return "", err
+		return nil, err
 	}
 	defer res.Body.Close()
 
 	out, err := ioutil.ReadAll(res.Body)
 	if err != nil {
-		return "", err
+		return nil, err
 	}
 
-	return string(out), nil
-
+	return out, nil
 }

+ 0 - 3
go/src/flow/flowserver/flowmodel.go

@@ -1,3 +0,0 @@
-package flowserver
-
-type Base64Data string

+ 4 - 0
go/src/flow/flowserver/flowutils.go

@@ -0,0 +1,4 @@
+package flowserver
+
+//Base64Data simple data to output base64 dataurl
+type Base64Data string

+ 56 - 23
go/src/flow/flowserver/session.go

@@ -179,15 +179,24 @@ func (s *FlowSession) Document(c *websocket.Conn) error {
 // NodeProcess a node triggering results
 // Build a flow and run
 func (s *FlowSession) NodeProcess(c *websocket.Conn, data []byte) error {
-	ID := string(data[1 : len(data)-1]) // remove " instead of unmarshalling json
+
 	if s.flow != nil {
-		s.Notify("a node is already running")
-		return errors.New("node already running")
+		s.Notify("flow is already running")
+		return errors.New("nodes already running")
 	}
 
+	ids := []string{}
+	err := json.Unmarshal(data, &ids)
+	if err != nil {
+		return err
+	}
+
+	// *New* 25-02-2018 node Array
+	//ID := string(data[1 : len(data)-1]) // remove " instead of unmarshalling json
+
 	// Clear activity
 	s.nodeActivity = map[string]*NodeActivity{}
-	s.Broadcast(nil, s.activity()) // Ampty activity
+	s.Broadcast(nil, s.activity()) // Empty activity in clients
 
 	build := func() error {
 
@@ -201,15 +210,24 @@ func (s *FlowSession) NodeProcess(c *websocket.Conn, data []byte) error {
 		localR.Add("Log", func() io.Writer {
 			return s
 		})
-		localR.Add("Output", func(d interface{}) {
-
-			r := fmt.Sprint("Result:", d)
+		// this will be disabled
+		localR.Add("Output", func(d interface{}) interface{} {
+			//r := fmt.Sprint("Result:", d)
 			// Do something
-			s.Notify(r)
-			s.Write([]byte(r))
+			//s.Notify(r)
+			//s.Write([]byte(r))
+			return d
 		})
+
 		builder := flowbuilder.New(localR)
-		builder.Load(s.RawDoc).Build(ID)
+		builder.Load(s.RawDoc)
+
+		ops := make([]flow.Operation, len(ids))
+
+		for i, id := range ids {
+			ops[i] = builder.Build(id)
+		}
+		// Multiple ops
 		if builder.Err != nil {
 			return builder.Err
 		}
@@ -225,13 +243,16 @@ func (s *FlowSession) NodeProcess(c *websocket.Conn, data []byte) error {
 		defer func() { // After routing gone
 			s.flow = nil
 		}()
-		// Flow activity
+
+		// Flow hooks
+		// Flow activity TODO: needs improvements as it shouldn't send the overall activity to client
+		// instead should send singular events
 		s.flow.Hook(flow.Hook{
-			Any: func(name string, triggerOp flow.Operation, triggerTime time.Time, extra ...flow.Data) {
+			Any: func(name string, hookOp flow.Operation, triggerTime time.Time, extra ...flow.Data) {
 				s.Lock()
 				defer s.Unlock()
 
-				nodeIDs := builder.GetOpIDs(triggerOp)
+				nodeIDs := builder.GetOpIDs(hookOp)
 				updated := true
 				for _, nodeID := range nodeIDs {
 					act, ok := s.nodeActivity[nodeID]
@@ -254,10 +275,18 @@ func (s *FlowSession) NodeProcess(c *websocket.Conn, data []byte) error {
 						act.EndTime = triggerTime
 						// only load data from requested node
 						// Or if node has the data retrieval flag
-						node := builder.Doc.FetchNodeByID(ID)
-						if node.Prop["data"] == "true" || nodeID == ID {
-							act.Data = extra[0]
+						// if running ids contains the nodeID
+						// we add the data
+						for _, id := range ids {
+							if nodeID == id {
+								act.Data = extra[0]
+							}
 						}
+						//node := builder.Doc.FetchNodeByID(ID)
+						//log.Println("Should we add data:", ID, node)
+						//if node.Prop["data"] == "true" || nodeID == ID {
+						//	act.Data = extra[0]
+						//}
 					case "Error":
 						status = "error"
 						act.EndTime = triggerTime
@@ -277,16 +306,18 @@ func (s *FlowSession) NodeProcess(c *websocket.Conn, data []byte) error {
 			},
 		})
 
-		op, ok := builder.OperationMap[ID]
+		/*op, ok := builder.OperationMap[ID]
 		if !ok {
 			return fmt.Errorf("Operation not found %v", ID)
-		}
+		}*/
 		log.Println("Processing operation")
-		_, err := op.Process()
+		sess := s.flow.NewSession()
+		_, err = sess.Run(ops...)
 		if err != nil {
 			log.Println("Error operation", err)
 			return err
 		}
+
 		log.Println("Experimental storing data to session")
 		// Copy Data from flow
 		s.flow.Data.Range(func(k, v interface{}) bool {
@@ -298,6 +329,7 @@ func (s *FlowSession) NodeProcess(c *websocket.Conn, data []byte) error {
 		return nil
 	}
 
+	// Background running
 	go func() {
 		err := build()
 		if err != nil {
@@ -332,11 +364,12 @@ func (s *FlowSession) NodeTrain(c *websocket.Conn, data []byte) error {
 		localR.Add("Log", func() io.Writer {
 			return s
 		})
-		localR.Add("Output", func(d interface{}) {
-			r := fmt.Sprint("Result:", d)
+		localR.Add("Output", func(d interface{}) interface{} {
+			//r := fmt.Sprint("Result:", d)
 			// Do something
-			s.Notify(r)
-			s.Write([]byte(r))
+			//s.Notify(r)
+			//s.Write([]byte(r))
+			return d
 		})
 		builder := flowbuilder.New(localR)
 		builder.Load(s.RawDoc).Build(ID)

+ 1 - 1
go/src/flow/session.go

@@ -35,7 +35,7 @@ func (s *Session) Run(ops ...Operation) ([]Data, error) {
 		oplist[i] = op.(*operation)
 	}
 
-	return s.runList(oplist, s.ginputs...)
+	return s.goRunList(oplist, s.ginputs...)
 }
 
 // The main run function?