//
// mods.js
//
// Neil Gershenfeld
// (c) Massachusetts Institute of Technology 2018
//
// This work may be reproduced, modified, distributed, performed, and
// displayed for any purpose, but must acknowledge the mods
// project. Copyright is retained and must be preserved. The work is
// provided as is; no warranty is provided, and users accept all
// liability.
//
// closure
//
(function(){
//
// globals
//
var mods = {}
mods.mod = {}
mods.globals = {}
mods.ui = {source:null,
   progname:'',
   padding:7,
   bezier:100,
   canvas:250,
   rows:5,
   cols:20,
   link_color:'rgb(0,0,128)',
   link_highlight:'rgb(255,0,0)',
   header:50,
   selected:{},
   mousedown:null,
   menu:null,
   top:null,
   left:null,
   xstart:null,
   ystart:null,
   xtrans:null,
   ytrans:null
   }
//
// UI
//
document.body.style.overflow = "hidden"
function mods_transform() {
   var transform = document.body.style.transform
   var m = new DOMMatrix(getComputedStyle(document.body).transform)
   var s = m.m11
   var tx = m.m41/s
   var ty = m.m42/s
   var origin = document.body.style.transformOrigin
      var pxx = origin.indexOf('px')
      var ox = parseFloat(origin.slice(0,pxx))
      var pxy = origin.indexOf('px',pxx+2)
      var oy = parseFloat(origin.slice(pxx+2,pxy))
   return({s:s,tx:tx,ty:ty,ox:ox,oy:oy})
   }
document.body.style.transform = 'scale(1) translate(0px,0px)'
document.body.style.transformOrigin = '0px 0px'
//
// scroll wheel event
//
window.addEventListener('wheel',function(evt) {
   /*
   (xw+tx-ox)*s+ox = xs
   xw = ox-tx+(xs-ox)/s
   (xw+tx0-ox0)*s+ox0  = (xw+tx1-ox1)*s+ox1
   (tx0-ox0)*s+ox0  = (tx1-ox1)*s+ox1
   tx0+(ox1-ox0)+(ox0-ox1)/s  = tx1
   tx0+(ox1-ox0)*(1-1/s)  = tx1
   */
   var el = document.elementFromPoint(evt.pageX,evt.pageY)
   if ((el.tagName == "HTML") || (el.tagName == "BODY")) {
      set_prompt('scroll to zoom')
      evt.preventDefault()
      evt.stopPropagation()
      var t = mods_transform()
      if (evt.deltaY > 0)
         var scale = t.s*1.1
      else
         var scale = t.s*0.9
      var tx = t.tx+(evt.pageX-t.ox)*(1-1/t.s)
      var ty = t.ty+(evt.pageY-t.oy)*(1-1/t.s)
      document.body.style.transform = `scale(${scale}) translate(${tx}px,${ty}px)`
      document.body.style.transformOrigin = `${evt.pageX}px ${evt.pageY}px`
      }
   })
//
// body mouse events
//
window.addEventListener('mousedown',function(evt) {
   //
   // get element mouse is over
   //
   var el = document.elementFromPoint(evt.pageX,evt.pageY)
   //
   // check if on body
   //
   if ((el.tagName == "HTML") || (el.tagName == "BODY")) {
      //
      // remember button
      //
      mods.ui.mousedown = evt.button
      if (mods.ui.mousedown == 0) {
         set_prompt('left-drag to pan, right-drag to select')
         if (mods.ui.menu != null) {
            document.body.removeChild(mods.ui.menu)
            mods.ui.menu = null
            }
         }
      else if (mods.ui.mousedown == 2) {
         set_prompt('menu; left-drag to pan, right-drag to select')
         }
      //
      // remember position
      //
      var t = mods_transform()
      mods.ui.xstart = evt.pageX
      mods.ui.ystart = evt.pageY
      mods.ui.xtrans = t.tx
      mods.ui.ytrans = t.ty
      }
   })
window.addEventListener('mousemove',function(evt) {
   //
   // mouse move
   //
   if (mods.ui.mousedown != null) {
      evt.preventDefault()
      evt.stopPropagation()
      var t = mods_transform()
      if (mods.ui.mousedown == 0) {
         //
         // pan on left drag
         //
         xtrans = mods.ui.xtrans+(evt.pageX-mods.ui.xstart)/t.s
         ytrans = mods.ui.ytrans+(evt.pageY-mods.ui.ystart)/t.s
         document.body.style.transform = `scale(${t.s}) translate(${xtrans}px,${ytrans}px)`
         }
      else if (mods.ui.mousedown == 2) {
         //
         // select on right drag
         //
         var rect = document.getElementById('svgrect')
         if (rect == undefined) {
            //
            // start dragging
            //
            if (mods.ui.menu != null) {
               document.body.removeChild(mods.ui.menu)
               mods.ui.menu = null
               }
            set_prompt('right-drag to select')
            var t = mods_transform()
            var rect = document.createElementNS('http://www.w3.org/2000/svg','rect')
               rect.setAttribute('id','svgrect')
               rect.setAttribute('x',t.ox-t.tx+(evt.pageX-t.ox)/t.s)
               rect.setAttribute('y',t.oy-t.ty+(evt.pageY-t.oy)/t.s-mods.ui.header)
               rect.setAttribute('width',0)
               rect.setAttribute('height',0)
               rect.setAttribute('fill','rgb(200,200,200)')
               rect.setAttribute('stroke','none')
            var svg = document.getElementById('svg')
               svg.insertBefore(rect,svg.firstChild)
            }
         else {
            //
            // continue dragging
            //
            var rect = document.getElementById('svgrect')
            var xp = t.ox-t.tx+(mods.ui.xstart-t.ox)/t.s
            var yp = t.oy-t.ty+(mods.ui.ystart-t.oy)/t.s-mods.ui.header
            var xw = t.ox-t.tx+(evt.pageX-t.ox)/t.s
            var yw = t.oy-t.ty+(evt.pageY-t.oy)/t.s-mods.ui.header
            if (xw < xp) {
               rect.setAttribute('x',xw)
               rect.setAttribute('width',xp-xw)
               }
            else
               rect.setAttribute('width',xw-xp)
            if (yw < yp) {
               rect.setAttribute('y',yw)
               rect.setAttribute('height',yp-yw)
               }
            else
               rect.setAttribute('height',yw-yp)
            }
         }
      }
   })
window.addEventListener('mouseup',function(evt) {
   //
   // mouse up
   //
   mods.ui.mousedown = null
   //
   // check for selection rectangle
   //
   var rect = document.getElementById('svgrect')
   if (rect != null) {
      //
      // rectangle exists, selecting modules
      //
      var x = parseFloat(rect.getAttribute('x'))
      var y = parseFloat(rect.getAttribute('y'))
      var width = parseFloat(rect.getAttribute('width'))
      var height = parseFloat(rect.getAttribute('height'))
      svg.removeChild(rect)
      var modules = document.getElementById('modules')
      //
      // loop to find selected modules
      //
      mods.ui.selected = {}
      for (var module in modules.childNodes) {
         var container = modules.childNodes[module]
         var id = container.id
         if (id != undefined) {
            var name = container.firstChild
            var left = parseFloat(container.dataset.left)
            var top = parseFloat(container.dataset.top)
            if ((x <= left) && (left <= x+width) && (y <= top) && (top <= y+height)) {
               //
               // module is in selection rectangle
               //
               name.style.fontWeight = "bold"
               mods.ui.selected[id] = true
               }
            else {
               //
               // module is not in selection rectangle
               //
               name.style.fontWeight = "normal"
               }
            }
         }
      }
   })
//
// context menu
//
window.addEventListener('contextmenu',function(evt){
   evt.stopPropagation()
   evt.preventDefault()
   if (mods.ui.menu != null) {
      document.body.removeChild(mods.ui.menu)
      mods.ui.menu = null
      }
   var div = document.createElement('div')
   make_menu(div)
   add_menu(div,'modules',modules)
   add_menu(div,'programs',programs)
   add_menu(div,'edit',edit)
   add_menu(div,'options',options)
   document.body.appendChild(div)
   function make_menu(div) {
      mods.ui.menu = div
      div.style.position = "absolute"
      var t = mods_transform()
      div.style.top = t.oy-t.ty+(evt.pageY-t.oy)/t.s
      div.style.left = t.ox-t.tx+(evt.pageX-t.ox)/t.s
      div.style.zIndex = 0
      div.style.cursor = 'default'
      div.style.backgroundColor = "rgb(220,255,255)"
      div.style.padding = 1.5*mods.ui.padding
      div.style.textAlign = 'left'
      div.style.border = '2px solid'
      div.style.borderRadius = '10px'
      }
   function add_menu(div,text,click) {
      var textdiv = document.createElement('div')
      textdiv.appendChild(document.createTextNode(text))
      textdiv.appendChild(document.createElement('br'))
      textdiv.addEventListener('mouseover',function(evt){
         evt.target.style.fontWeight = 'bold'})
      textdiv.addEventListener('mouseout',function(evt){
         evt.target.style.fontWeight = 'normal'})
      textdiv.addEventListener('mousedown',click)
      textdiv.addEventListener('touchend',click)
      div.appendChild(textdiv)
      }
   //
   // modules menu
   //
   function modules(evt) {
      evt.preventDefault()
      evt.stopPropagation()
      document.body.removeChild(evt.target.parentNode)
      var div = document.createElement('div')
      make_menu(div)
      set_prompt('modules')
      //
      // open server module
      //
      add_menu(div,'open server module',function(evt){
         function module_label(label) {
            var div = document.createElement('div')
            var i = document.createElement('i')
            i.appendChild(document.createTextNode(label))
            div.appendChild(i)
            div.appendChild(document.createElement('br'))
            menu.appendChild(div)
            }
         function module_menu(label,module) {
            var div = document.createElement('div')
            div.appendChild(
               document.createTextNode('\u00A0\u00A0\u00A0'+label))
            div.addEventListener('mouseover',function(evt){
               evt.target.style.fontWeight = 'bold'})
            div.addEventListener('mouseout',function(evt){
               evt.target.style.fontWeight = 'normal'})
            div.addEventListener('mousedown',function(evt){
               evt.preventDefault()
               evt.stopPropagation()
               document.body.removeChild(evt.target.parentNode)
               mods.ui.menu = null
               var t = mods_transform()
               mod_message_handler(module,
                  t.oy-t.ty+(evt.pageY-t.oy)/t.s,
                  t.ox-t.tx+(evt.pageX-t.ox)/t.s)})
            div.appendChild(document.createElement('br'))
            menu.appendChild(div)
            }
         document.body.removeChild(evt.target.parentNode)
         var menu = document.createElement('div')
         make_menu(menu)
         document.body.appendChild(menu)
         menu.style.width = mods.ui.canvas
         menu.style.height = mods.ui.canvas
         menu.style.overflow = 'auto'
         var req = new XMLHttpRequest()
         req.responseType = 'text'
         req.onreadystatechange = function() {
            if (req.readyState == XMLHttpRequest.DONE) {
               var str = req.response
               eval(str)
               }
            }
         req.open('GET','modules/index.js'+'?rnd='+Math.random())
         req.send()
         })
      //
      // open local module
      //
      add_menu(div,'open local module',function(evt){
         var t = mods_transform()
         mods.ui.top = t.oy-t.ty+(evt.pageY-t.oy)/t.s
         mods.ui.left = t.ox-t.tx+(evt.pageX-t.ox)/t.s
         document.body.removeChild(evt.target.parentNode)
         mods.ui.menu = null
         var file = document.getElementById('mod_input')
         file.value = null
         file.click()
         })
      //
      // open remote module
      //
      add_menu(div,'open remote module',function(evt){
         document.body.removeChild(evt.target.parentNode)
         mods.ui.menu = null
         set_prompt('remotes not yet implemented')
         })
      document.body.appendChild(div)
      }
   //
   // programs menu
   //
   function programs(evt) {
      evt.preventDefault()
      evt.stopPropagation()
      document.body.removeChild(evt.target.parentNode)
      var div = document.createElement('div')
      make_menu(div)
      set_prompt('programs')
      //
      // open local program
      //
      add_menu(div,'open local program',function(evt){
         document.body.removeChild(evt.target.parentNode)
         mods.ui.menu = null
         var file = document.getElementById('prog_input')
         file.value = null
         file.click()
         })
      //
      // open server program
      //
      add_menu(div,'open server program',function(evt){
         function program_label(label) {
            var div = document.createElement('div')
            var i = document.createElement('i')
            i.appendChild(document.createTextNode(label))
            div.appendChild(i)
            div.appendChild(document.createElement('br'))
            menu.appendChild(div)
            }
         function program_menu(label,program) {
            var div = document.createElement('div')
            div.appendChild(
               document.createTextNode('\u00A0\u00A0\u00A0'+label))
            div.addEventListener('mouseover',function(evt){
               evt.target.style.fontWeight = 'bold'})
            div.addEventListener('mouseout',function(evt){
               evt.target.style.fontWeight = 'normal'})
            div.addEventListener('mousedown',function(evt){
               evt.preventDefault()
               evt.stopPropagation()
               if (location.port == 80)
                  var uri = 'http://'+location.hostname
                     +'?program='+program
               else
                  var uri = 'http://'+location.hostname+':'
                     +location.port+'?program='+program
               set_prompt('<a href='+uri+'>program link</a>')
               prog_message_handler(program)
               document.body.removeChild(evt.target.parentNode)
               mods.ui.menu = null
               })
            div.appendChild(document.createElement('br'))
            menu.appendChild(div)
            }
         document.body.removeChild(evt.target.parentNode)
         var menu = document.createElement('div')
         make_menu(menu)
         document.body.appendChild(menu)
         menu.style.width = mods.ui.canvas
         menu.style.height = mods.ui.canvas
         menu.style.overflow = 'auto'
         var req = new XMLHttpRequest()
         req.responseType = 'text'
         req.onreadystatechange = function() {
            if (req.readyState == XMLHttpRequest.DONE) {
               var str = req.response
               eval(str)
               }
            }
         req.open('GET','programs/index.js'+'?rnd='+Math.random())
         req.send()
         })
      //
      // open remote program
      //
      add_menu(div,'open remote program',function(evt){
         document.body.removeChild(evt.target.parentNode)
         mods.ui.menu = null
         set_prompt('remotes not yet implemented')
         })
      //
      // save local program
      //
      add_menu(div,'save local program',function(evt){
         document.body.removeChild(evt.target.parentNode)
         mods.ui.menu = null
         save_program()
         })
      //
      // save local page
      //
      add_menu(div,'save local page',function(evt){
         document.body.removeChild(evt.target.parentNode)
         mods.ui.menu = null
         save_page()
         })
      document.body.appendChild(div)
      }
   //
   // edit menu
   //
   function edit(evt) {
      evt.preventDefault()
      evt.stopPropagation()
      document.body.removeChild(evt.target.parentNode)
      var div = document.createElement('div')
      make_menu(div)
      set_prompt('edit')
      //
      // cut
      //
      add_menu(div,'cut',function(evt){
         evt.preventDefault()
         evt.stopPropagation()
         document.body.removeChild(evt.target.parentNode)
         mods.ui.menu = null
         if ((Object.keys(mods.ui.selected).length) == 0) {
            set_prompt("nothing selected")
            }
         else {
            for (var id in mods.ui.selected) {
               var div = document.getElementById(id)
               delete_module(id)
               mods.ui.selected = {}
               }
            }
         })
      //
      // copy
      //
      add_menu(div,'copy',function(evt){
         evt.preventDefault()
         evt.stopPropagation()
         document.body.removeChild(evt.target.parentNode)
         mods.ui.menu = null
         set_prompt('copy not yet implemented')
         })
      //
      // paste
      //
      add_menu(div,'paste',function(evt){
         evt.preventDefault()
         evt.stopPropagation()
         document.body.removeChild(evt.target.parentNode)
         mods.ui.menu = null
         set_prompt('paste not yet implemented')
         })
      document.body.appendChild(div)
      }
   //
   // options menu
   //
   function options(evt) {
      evt.preventDefault()
      evt.stopPropagation()
      document.body.removeChild(evt.target.parentNode)
      mods.ui.menu = null
      var div = document.createElement('div')
      make_menu(div)
      set_prompt('options')
      //
      // view files
      //
      add_menu(div,'view files',function(evt){
         document.body.removeChild(evt.target.parentNode)
         mods.ui.menu = null
         var win = window.open('files.html')
         })
      document.body.appendChild(div)
      //
      // view project
      //
      add_menu(div,'view project',function(evt){
         document.body.removeChild(evt.target.parentNode)
         mods.ui.menu = null
         var win = window.open('https://gitlab.cba.mit.edu/pub/mods')
         })
      document.body.appendChild(div)
      }
   })
//
// prompt
//
document.body.appendChild(document.createTextNode(' '))
var span = document.createElement('span')
   span.setAttribute('id','logo')
   span.style.display = 'inline-block'
   span.style.verticalAlign = 'middle'
   span.style.width = 20
   span.style.height = 20
   span.style.padding = mods.ui.padding
   span.appendChild(logo(1))
   document.body.appendChild(span)
document.body.appendChild(document.createTextNode(' '))
var span = document.createElement('span')
   span.setAttribute('id','prompt')
   span.style.display = 'inline-block'
   span.style.verticalAlign = 'middle'
   var innerspan = document.createElement('span')
      span.appendChild(innerspan)
   document.body.appendChild(span)
function logo(size) {
   var x = 0
   var y = 2.8*size/3.8
   var svgNS = "http://www.w3.org/2000/svg"
   var logo = document.createElementNS(svgNS,"svg")
   logo.setAttributeNS("http://www.w3.org/2000/xmlns/",
      "xmlns:xlink","http://www.w3.org/1999/xlink")
   logo.setAttributeNS(null,'viewBox',"0 0 "+size+" "+size)
   var new_rect = document.createElementNS(svgNS,"rect");
   new_rect.setAttribute("width",size/3.8)
   new_rect.setAttribute("height",size/3.8)
   new_rect.setAttribute("x",x)
   new_rect.setAttribute("y",y)
   new_rect.setAttribute("fill","blue")
   logo.appendChild(new_rect)
   var new_rect = document.createElementNS(svgNS,"rect");
   new_rect.setAttribute("width",size/3.8)
   new_rect.setAttribute("height",size/3.8)
   new_rect.setAttribute("x",x+1.4*size/3.8)
   new_rect.setAttribute("y",y)
   new_rect.setAttribute("fill","blue")
   logo.appendChild(new_rect)
   var new_rect = document.createElementNS(svgNS,"rect");
   new_rect.setAttribute("width",size/3.8)
   new_rect.setAttribute("height",size/3.8)
   new_rect.setAttribute("x",x+2.8*size/3.8)
   new_rect.setAttribute("y",y)
   new_rect.setAttribute("fill","blue")
   logo.appendChild(new_rect)
   var new_rect = document.createElementNS(svgNS, "rect");
   new_rect.setAttribute("width",size/3.8)
   new_rect.setAttribute("height",size/3.8)
   new_rect.setAttribute("x",x)
   new_rect.setAttribute("y",y-1.4*size/3.8)
   new_rect.setAttribute("fill","blue")
   logo.appendChild(new_rect)
   var new_rect = document.createElementNS(svgNS, "rect");
   new_rect.setAttribute("width", size / 3.8)
   new_rect.setAttribute("height", size / 3.8)
   new_rect.setAttribute("x", x + 2.8 * size / 3.8)
   new_rect.setAttribute("y", y - 1.4 * size / 3.8)
   new_rect.setAttribute("fill", "blue")
   logo.appendChild(new_rect)
   var new_rect = document.createElementNS(svgNS, "rect");
   new_rect.setAttribute("width", size / 3.8)
   new_rect.setAttribute("height", size / 3.8)
   new_rect.setAttribute("x", x + 1.4 * size / 3.8)
   new_rect.setAttribute("y", y - 2.8 * size / 3.8)
   new_rect.setAttribute("fill", "blue")
   logo.appendChild(new_rect)
   var new_rect = document.createElementNS(svgNS, "rect");
   new_rect.setAttribute("width", size / 3.8)
   new_rect.setAttribute("height", size / 3.8)
   new_rect.setAttribute("x", x + 2.8 * size / 3.8)
   new_rect.setAttribute("y", y - 2.8 * size / 3.8)
   new_rect.setAttribute("fill", "blue")
   logo.appendChild(new_rect)
   var new_circ = document.createElementNS(svgNS, "circle");
   new_circ.setAttribute("r", size / (2 * 3.8))
   new_circ.setAttribute("cx", x + size / (2 * 3.8))
   new_circ.setAttribute("cy", y + size / (2 * 3.8) - 2.8 * size / 3.8)
   new_circ.setAttribute("fill", "red")
   logo.appendChild(new_circ)
   var new_circ = document.createElementNS(svgNS, "circle");
   new_circ.setAttribute("r", size / (2 * 3.8))
   new_circ.setAttribute("cx", x + size / (2 * 3.8) + 1.4 * size / 3.8)
   new_circ.setAttribute("cy", y + size / (2 * 3.8) - 1.4 * size / 3.8)
   new_circ.setAttribute("fill", "red")
   logo.appendChild(new_circ)
   return logo
   }
set_prompt('right click/two finger/long press for menu; scroll for zoom, drag for pan')
//
// SVG canvas for drawing
//
var svg = document.createElementNS("http://www.w3.org/2000/svg", "svg")
   svg.style.position = 'absolute'
   svg.style.backgroundColor = 'rgb(255,255,255)'
   svg.style.top = mods.ui.header
   svg.style.left = 0
   svg.style.zIndex = 0
   svg.style.overflow = 'visible'
   svg.setAttribute('width',40)
   svg.setAttribute('height',40)
   svg.setAttribute('id','svg')
   svg.setAttributeNS("http://www.w3.org/2000/xmlns/","xmlns:xlink","http://www.w3.org/1999/xlink")
   document.body.appendChild(svg)
//
// link container
//
var svg = document.getElementById('svg')
var g = document.createElementNS('http://www.w3.org/2000/svg','g')
   g.setAttribute('id','links')
   svg.appendChild(g)
//
// file reading controls
//
var file = document.createElement('input')
   file.setAttribute('type','file')
   file.setAttribute('id','mod_input')
   file.style.position = 'absolute'
   file.style.left = 0
   file.style.top = 0
   file.style.width = 0
   file.style.height = 0
   file.style.opacity = 0
   file.addEventListener('change',function() {
      mod_read_handler()
      })
   document.body.appendChild(file)
var file = document.createElement('input')
   file.setAttribute('type','file')
   file.setAttribute('id','prog_input')
   file.style.position = 'absolute'
   file.style.left = 0
   file.style.top = 0
   file.style.width = 0
   file.style.height = 0
   file.style.opacity = 0
   file.addEventListener('change',function() {
      prog_read_handler()
      })
   document.body.appendChild(file)
//
// module container
//
var div = document.createElement('div')
   div.setAttribute('id','modules')
   document.body.appendChild(div)
//
// check for program load query
//
if (location.search.length > 0) {
   var args = location.search.slice(1).split('&')
   for (var a in args) {
      var arg = args[a].split('=')
      if (arg[0] == 'program')
      prog_message_handler(arg[1])
      }
   }
//
// program routines
//
function prog_read_handler(event) {
   var file = document.getElementById('prog_input')
   var file_reader = new FileReader()
   file_reader.onload = prog_load_handler
   file_reader.readAsText(file.files[0])
   mods.ui.progname = file.files[0].name
   }
function prog_message_handler(filename) {
   var req = new XMLHttpRequest()
   req.responseType = 'text'
   req.onreadystatechange = function() {
      if (req.readyState == XMLHttpRequest.DONE) {
         prog = JSON.parse(req.response)
         prog_load(prog)
         }
      }
   var index = filename.lastIndexOf('/')
   if (index != -1) {
      mods.ui.progname = filename.slice(index+1)
      }
   else
      mods.ui.progname = filename
   //
   // send request, with random query to prevent caching
   //
   req.open('GET',filename+'?rnd='+Math.random())
   req.send()
   }
function prog_load_handler(event) {
   prog = JSON.parse(event.target.result)
   prog_load(prog)
   }
function prog_load(prog) {
   //
   // load modules
   //
   for (var idnumber in prog.modules) {
      var module = prog.modules[idnumber]
      var str = module.definition
      try {
         eval('var args = '+str)
         }
      catch (err) {
         console.log(err.message)
         return
         }
      args.definition = str
      args.id = idnumber
      var t = mods_transform()
      var xw = t.ox-t.tx+(mods.ui.xstart-t.ox)/t.s
      var yw = t.oy-t.ty+(mods.ui.ystart-t.oy)/t.s-mods.ui.header
      args.top = parseFloat(module.top)+yw
      args.left = parseFloat(module.left)+xw
      args.filename = module.filename
      add_module(args)
      }
   //
   // load links
   //
   for (var linkid in prog.links) {
      var str = prog.links[linkid]
      eval('var link = '+str)
      eval('var linksrc = '+link.source)
      eval('var linkdst = '+link.dest)
      var src = document.getElementById(
         JSON.stringify({id:linksrc.id,type:linksrc.type,name:linksrc.name}))
      var dst = document.getElementById(
         JSON.stringify({id:linkdst.id,type:linkdst.type,name:linkdst.name}))
      add_link(src,dst)
      }
   }
function save_program() {
   set_prompt('program name? ')
   get_prompt(mods.ui.progname,function(filename){
      mods.ui.progname = filename
      var prog = {modules:{},links:[]}
      var modules = document.getElementById('modules')
      //
      // save modules
      //
      for (var c = 0; c < modules.childNodes.length; ++c) {
         var module = modules.childNodes[c]
         var idnumber = module.id
         prog.modules[idnumber] = {
            definition:update_module_definition(
               idnumber,module.dataset.definition),
            top:module.dataset.top,
            left:module.dataset.left,
            filename: module.dataset.filename,
            inputs:{},
            outputs:{}
            }
         }
      //
      // save links
      //
      var svg = document.getElementById('svg')
      var links = svg.getElementById('links')
         for (var l = 0; l < links.childNodes.length; ++l) {
            var link = links.childNodes[l]
            var linkid = link.id
            prog.links.push(linkid)
            }
      //
      // download
      //
      var text = JSON.stringify(prog)
      var a = document.createElement('a')
      a.setAttribute('href','data:text/plain;charset=utf-8,'+
         encodeURIComponent(text))
      a.setAttribute('download',filename)
      a.style.display = 'none'
      document.body.appendChild(a)
      a.click()
      document.body.removeChild(a)
      })
   }
function save_page() {
   set_prompt('page name? ')
   get_prompt(mods.ui.progname+".html",function(filename){
      mods.ui.progname = filename
      var prog = {modules:{},links:[]}
      var modules = document.getElementById('modules')
      //
      // save modules
      //
      for (var c = 0; c < modules.childNodes.length; ++c) {
         var module = modules.childNodes[c]
         var idnumber = module.id
         prog.modules[idnumber] = {
            definition:update_module_definition(
               idnumber,module.dataset.definition),
            top:module.dataset.top,
            left:module.dataset.left,
	    filename: module.dataset.filename,
            inputs:{},
            outputs:{}
            }
         }
      //
      // save links
      //
      var svg = document.getElementById('svg')
      var links = svg.getElementById('links')
         for (var l = 0; l < links.childNodes.length; ++l) {
            var link = links.childNodes[l]
            var linkid = link.id
            prog.links.push(linkid)
            }
      //
      // read mods.js
      //
      var req = new XMLHttpRequest()
      req.responseType = 'text'
      req.onreadystatechange = function() {
         if (req.readyState == XMLHttpRequest.DONE) {
            //
            // construct page
            //
            var str = req.response
            var text ="<html>\n"
            text += "<head><meta charset='utf-8'>\n"
            text += "<title>mods</title>\n"
            text += "</head>\n"
            text += "<body link='black' alink='black' vlink='black'>\n"
            text += "<"+"script"+">\n"
            text += str
            text += "var prog = JSON.parse(JSON.stringify("+JSON.stringify(prog)+"))\n"
            text += "window.mods_prog_load(prog)\n"
            text += "</"+"script"+">\n"
            text += "</body>\n"
            text += "</html>\n"
            //
            // download page
            //
            var a = document.createElement('a')
            a.setAttribute('href','data:text/plain;charset=utf-8,'+
               encodeURIComponent(text))
            a.setAttribute('download',filename)
            a.style.display = 'none'
            document.body.appendChild(a)
            a.click()
            document.body.removeChild(a)
            }
         }
      //
      // send request, with random query to prevent caching
      //
      req.open('GET','js/mods.js'+'?rnd='+Math.random())
      req.send()
      })
   }
//
// add program load to window
//
window.mods_prog_load = function(prog) {
   prog_load(prog)
   }
//
// module routines
//
function mod_read_handler(event) {
   var file = document.getElementById('mod_input')
   var file_reader = new FileReader()
   file_reader.onload = mod_load_handler
   file_reader.readAsText(file.files[0])
   }
function mod_message_handler(filename,top,left) {
   var req = new XMLHttpRequest()
   req.responseType = 'text'
   req.onreadystatechange = function() {
      if (req.readyState == XMLHttpRequest.DONE) {
         var str = req.response
         try {
            eval('var args = '+str)
            }
         catch (err) {
            console.log(err.message)
            return
            }
         args.definition = str
         args.id = String(Math.random())
         args.top = top
         args.left = left
         args.filename = filename
         add_module(args)
         }
      }
   //
   // send request, with random query to prevent caching
   //
   req.open('GET',filename+'?rnd='+Math.random())
   req.send()
   }
function mod_load_handler(event) {
   str = event.target.result
   try {
      eval('var args = '+str)
      }
   catch (err) {
      console.log(err.message)
      return
      }
   args.definition = str
   args.id = String(Math.random())
   args.top = mods.ui.top
   args.left = mods.ui.left
   args.filename = ""
   var div = add_module(args)
   return(div)
   }
function add_module(args) {
   var idnumber = args.id
   mods.mod[idnumber] = args.mod
   var modules = document.getElementById('modules')
   //
   // container
   //
   var container = document.createElement('div')
      container.setAttribute("id",idnumber)
      container.style.position = "absolute"
      container.style.top = args.top
      container.style.left = args.left
      container.dataset.top = args.top
      container.dataset.left = args.left
      container.dataset.filename = args.filename
      container.dataset.name = args.name
      container.style.zIndex = 0
      container.style.width = window.innerWidth
      container.dataset.definition = args.definition
      modules.appendChild(container)
   //
   // name
   //
   var divname = document.createElement('div')
      divname.appendChild(document.createTextNode(args.name))
      divname.addEventListener('mouseover',name_over)
      divname.addEventListener('mouseout',name_out)
      divname.addEventListener('mousedown',name_mousedown)
      divname.addEventListener('touchstart',name_touchdown)
      divname.style.backgroundColor = "rgb(210,240,210)"
      divname.style.padding = 1.5*mods.ui.padding
      divname.style.position = "absolute"
      divname.style.cursor = 'default'
      divname.style.top = 0
      divname.style.left = 0
      divname.style.textAlign = 'center'
      divname.style.border = '2px solid'
      divname.style.borderRadius = '10px'
      container.appendChild(divname)
   //
   // controls
   //
   var divctrl = document.createElement('div')
      var editspan = document.createElement('span')
         editspan.innerHTML = 'edit'
         editspan.style.fontWeight = 'normal'
         editspan.addEventListener('mouseover',function(event){
            set_prompt('click to edit')
            editspan.style.fontWeight = 'bold'})
         editspan.addEventListener('mouseout',function(event){
            set_prompt('')
            editspan.style.fontWeight = 'normal'})
         editspan.addEventListener('mousedown',edit_module)
         divctrl.appendChild(editspan)
      var delspan = document.createElement('span')
         delspan.innerHTML = ' delete '
         delspan.addEventListener('mouseover',function(event){
            set_prompt('click to delete')
            delspan.style.fontWeight = 'bold'})
         delspan.addEventListener('mouseout',function(event){
            set_prompt('')
            delspan.style.fontWeight = 'normal'})
         delspan.addEventListener('mousedown',function(event){
            delete_module(event.target.parentNode.parentNode.id)})
         divctrl.appendChild(delspan)
      divctrl.style.backgroundColor = "rgb(240,220,220)"
      divctrl.style.padding = mods.ui.padding
      divctrl.style.position = "absolute"
      divctrl.style.cursor = 'default'
      divctrl.style.top = divname.clientHeight
      divctrl.style.left = 0
      divctrl.style.textAlign = 'center'
      divctrl.style.border = '2px solid'
      divctrl.style.borderRadius = '10px'
      container.appendChild(divctrl)
      divctrl.style.left = divname.clientWidth/2-divctrl.clientWidth/2
   //
   // interface
   //
   var divint = document.createElement('div')
      divint.style.backgroundColor = "rgb(240,240,240)"
      divint.style.padding = mods.ui.padding
      divint.style.position = "absolute"
      divint.style.top = divname.clientHeight+divctrl.clientHeight
      divint.style.textAlign = 'center'
      divint.style.border = '2px solid'
      divint.style.borderRadius = '10px'
      divint.setAttribute('id',JSON.stringify({id:idnumber,type:'interface'}))
      divint.dataset.id = idnumber
      divint.dataset.divNameSize = divname.clientWidth
      args.interface(divint)
      container.appendChild(divint)
      divint.style.left = divname.clientWidth/2-divint.clientWidth/2
   //
   // inputs
   //
   var divin = document.createElement('div')
      divin.setAttribute('id',JSON.stringify({id:idnumber,type:'inputs'}))
      var b = document.createElement('b')
         b.appendChild(document.createTextNode('inputs'))
         divin.appendChild(b)
      divin.style.backgroundColor = "rgb(240,240,210)"
      divin.style.padding = mods.ui.padding
      divin.style.paddingLeft = '2px'
      divin.style.position= "absolute"
      divin.style.top = divname.clientHeight+divctrl.clientHeight
      divin.style.textAlign = 'left'
      divin.style.border = '2px solid'
      divin.style.borderRadius = '10px'
      divin.setAttribute('id',JSON.stringify({id:idnumber,type:'inputs'}))
      divin.style.cursor = 'default'
      divin.addEventListener('mousedown',nothing)
      divin.addEventListener('touchstart',nothing)
      divin.addEventListener('mouseup',nothing)
      divin.addEventListener('touchend',nothing)
      for (var v in args.inputs) {
         var div = document.createElement('div')
         if (args.inputs[v].label != undefined)
            div.innerHTML += args.inputs[v].label
         else
            div.innerHTML += v
         if (args.inputs[v].type != '')
            div.innerHTML += ' ('+args.inputs[v].type+')'
         div.setAttribute('id',JSON.stringify({id:idnumber,type:'inputs',name:v}))
         div.addEventListener('mouseover',input_over)
         div.addEventListener('mouseout',input_out)
         div.addEventListener('mousedown',input_mousedown)
         div.addEventListener('touchstart',input_touchdown)
         div.dataset.links = JSON.stringify([])
         div.dataset.name = v
         divin.appendChild(div)
         div.dataset.id = JSON.stringify(
            {id:idnumber,type:'input',name:v,
            rnd:Math.random()}) // randomize for unique events
         window.addEventListener(div.dataset.id,args.inputs[v].event)
         }
      container.appendChild(divin)
      if ((Object.keys(args.inputs).length) == 0)
         divin.style.visibility = 'hidden'
      divin.style.left = divname.clientWidth/2
         -divint.clientWidth/2-divin.clientWidth
      for (var i = 1; i < divin.childNodes.length; ++i) {
         divin.childNodes[i].dataset.dx = divin.offsetLeft
            +divin.childNodes[i].offsetLeft
         divin.childNodes[i].dataset.dy = divin.offsetTop
            +divin.childNodes[i].offsetTop
            +divin.childNodes[i].offsetHeight/2
         }
   //
   // outputs
   //
   var divout = document.createElement('div')
      divout.setAttribute('id',JSON.stringify({id:idnumber,type:'outputs'}))
      var b = document.createElement('b')
         b.appendChild(document.createTextNode('outputs'))
      divout.appendChild(b)
      divout.style.backgroundColor = "rgb(240,240,210)"
      divout.style.padding = mods.ui.padding
      divout.style.paddingRight = '2px'
      divout.style.position = "absolute"
      divout.style.top = divname.clientHeight+divctrl.clientHeight
      divout.style.textAlign = 'right'
      divout.addEventListener('mousedown',nothing)
      divout.style.border = '2px solid'
      divout.style.borderRadius = '10px'
      divout.setAttribute('id',JSON.stringify({id:idnumber,type:'outputs'}))
      divout.style.cursor = 'default'
      divout.addEventListener('touchstart',nothing)
      divout.addEventListener('mouseup',nothing)
      divout.addEventListener('touchend',nothing)
      for (var v in args.outputs) {
         var div = document.createElement('div')
         if (args.outputs[v].label != undefined)
            div.innerHTML += args.outputs[v].label
         else
            div.innerHTML += v
         if (args.outputs[v].type != '')
            div.innerHTML += ' ('+args.outputs[v].type+')'
         div.setAttribute('id',JSON.stringify({id:idnumber,type:'outputs',name:v}))
         div.addEventListener('mouseover',output_over)
         div.addEventListener('mouseout',output_out)
         div.addEventListener('mousedown',output_mousedown)
         div.addEventListener('touchstart',output_touchdown)
         div.dataset.links = JSON.stringify([])
         div.dataset.name = v
         divout.appendChild(div)
         div.dataset.id = JSON.stringify(
            {id:idnumber,type:'output',name:v,
            rnd:Math.random()}) // randomize for unique events
         window.addEventListener(div.dataset.id,args.outputs[v].event)
         }
      container.appendChild(divout)
      if ((Object.keys(args.outputs).length) == 0)
         divout.style.visibility = 'hidden'
      divout.style.left = divname.clientWidth/2
         +divint.clientWidth/2
      for (var i = 1; i < divout.childNodes.length; ++i) {
         divout.childNodes[i].dataset.dx = divout.offsetLeft
            +divout.childNodes[i].offsetLeft
            +divout.childNodes[i].offsetWidth
         divout.childNodes[i].dataset.dy = divout.offsetTop
            +divout.childNodes[i].offsetTop
            +divout.childNodes[i].offsetHeight/2
         }
      //
      // initialization
      //
      args.init()
      //
      // resize to contents
      //
      container.style.width = divint.clientWidth+divin.clientWidth+divout.clientWidth
      mods.fit(divint)
      //
      // return container
      //
      return(container)
   }
function delete_module(idnumber) {
   //
   // delete links
   //
   var ins = document.getElementById(
      JSON.stringify({id:idnumber,type:'inputs'}))
   var outs = document.getElementById(
      JSON.stringify({id:idnumber,type:'outputs'}))
   for (var i = 1; i < ins.childNodes.length; ++i) {
      var links = JSON.parse(ins.childNodes[i].dataset.links)
      for (var l in links)
         delete_link(links[l])
      }
   for (var i = 1; i < outs.childNodes.length; ++i) {
      var links = JSON.parse(outs.childNodes[i].dataset.links)
      for (var l in links)
         delete_link(links[l])
      }
   //
   // delete container
   //
   var modules = document.getElementById('modules')
   var container = document.getElementById(idnumber)
   modules.removeChild(container)
   //
   // clear prompt
   //
   set_prompt('')
   }
function update_module_definition(id) {
   //
   // get definition
   //
   var module = document.getElementById(id)
   var def = module.dataset.definition
   //
   // check for mod
   //
   if (mods.mod[id] == undefined)
      return def
   //
   // split definition
   //
   var lines = def.split('\n')
   //
   // find init function
   //
   var line = 0
   while (line < lines.length) {
      if (lines[line].indexOf("var init") == 0)
         break
      line += 1
      }
   //
   // read initializations up to inputs function
   //
   while (line < lines.length) {
      if (lines[line].indexOf(".value =") != -1) {
         var start = 4+lines[line].indexOf("mod.")
         var end = lines[line].indexOf(".value")
         var key = lines[line].slice(start,end)
         var value = mods.mod[id][key]['value']
         if (value.indexOf('\n') != -1)
            value = value.replace(/\n/g,"\\n")
         lines[line] = "   mod."+key+".value = '"+value+"'"
         }
      else if (lines[line].indexOf(".checked =") != -1) {
         var start = 4+lines[line].indexOf("mod.")
         var end = lines[line].indexOf(".checked")
         var key = lines[line].slice(start,end)
         var value = mods.mod[id][key]['checked']
         lines[line] = "   mod."+key+".checked = "+value
         }
      if (lines[line].indexOf("var inputs") == 0)
         break
      line += 1
      }
   return(lines.join('\n'))
   }
function edit_module(evt) {
   var mod = evt.target.parentNode.parentNode
   var idnumber = mod.id
   var def = update_module_definition(idnumber)
   //
   // open edit window
   //
   var top = mod.dataset.top
   var left = mod.dataset.left
   var name = mod.dataset.name
   var filename = mod.dataset.filename
   var fontsize = 100
   var win = window.open('')
   var file = document.createElement('input')
      file.setAttribute('type','file')
      file.setAttribute('id','edit_module_file')
      file.style.position = 'absolute'
      file.style.left = 0
      file.style.top = 0
      file.style.width = 0
      file.style.height = 0
      file.style.opacity = 0
      file.addEventListener('change',function() {
         edit_module_read_handler()
         })
      win.document.body.appendChild(file)
   function edit_module_read_handler() {
      var file = win.document.getElementById('edit_module_file')
      var file_reader = new FileReader()
      file_reader.onload = edit_module_load_handler
      file_reader.readAsText(file.files[0])
      }
   function edit_module_load_handler(event) {
      str = event.target.result
      var text = win.document.getElementById('edit_module_text')
      text.value = str
      update_module(idnumber)
      win.close()
      }
   if (filename != "undefined" && filename != "") {
      var btn = document.createElement('button')
         btn.appendChild(document.createTextNode('reload from server'))
         btn.style.padding = mods.ui.padding
         btn.style.margin = 1
         btn.addEventListener('click',function(){
            var req = new XMLHttpRequest()
            req.responseType = 'text'
            req.onreadystatechange = function() {
               if (req.readyState == XMLHttpRequest.DONE) {
                  text.value = req.response
                  update_module(idnumber)
                  }
               }
            req.open('GET',filename+'?rnd='+Math.random())
            req.send()
            win.close()
            })
         win.document.body.appendChild(btn)
      }
   var btn = document.createElement('button')
      btn.appendChild(document.createTextNode('load from file'))
      btn.style.padding = mods.ui.padding
      btn.style.margin = 1
      btn.addEventListener('click',function(){
         var file = win.document.getElementById('edit_module_file')
         file.value = null
         file.click()
         })
      win.document.body.appendChild(btn)
   var btn = document.createElement('button')
      btn.appendChild(document.createTextNode('update and close'))
      btn.style.padding = mods.ui.padding
      btn.style.margin = 1
      btn.addEventListener('click',function(){
         update_module(idnumber)
         win.close()
         })
      win.document.body.appendChild(btn)
   var btn = document.createElement('button')
      btn.appendChild(document.createTextNode('update'))
      btn.style.padding = mods.ui.padding
      btn.style.margin = 1
      btn.addEventListener('click',function(){
         update_module(idnumber)
         })
      win.document.body.appendChild(btn)
   var btn = document.createElement('button')
      btn.appendChild(document.createTextNode('close'))
      btn.style.padding = mods.ui.padding
      btn.style.margin = 1
      btn.addEventListener('click',function(){
         win.close()
         })
      win.document.body.appendChild(btn)
   var btn = document.createElement('button')
      btn.appendChild(document.createTextNode('save'))
      btn.style.padding = mods.ui.padding
      btn.style.margin = 1
      btn.addEventListener('click',function(){
         var a = document.createElement('a')
         a.setAttribute('href','data:text/plain;charset=utf-8,'+
            encodeURIComponent(text.value))
         a.setAttribute('download',name)
         a.style.display = 'none'
         document.body.appendChild(a)
         a.click()
         document.body.removeChild(a)
         })
      win.document.body.appendChild(btn)
   var btn = document.createElement('button')
      btn.appendChild(document.createTextNode('increase font'))
      btn.style.padding = mods.ui.padding
      btn.style.margin = 1
      btn.addEventListener('click',function(){
         fontsize *= 1.2
         text.style.fontSize = fontsize+'%'
         })
      win.document.body.appendChild(btn)
   var btn = document.createElement('button')
      btn.appendChild(document.createTextNode('decrease font'))
      btn.style.padding = mods.ui.padding
      btn.style.margin = 1
      btn.addEventListener('click',function(){
         fontsize /= 1.2
         text.style.fontSize = fontsize+'%'
         })
      win.document.body.appendChild(btn)
   win.document.body.appendChild(document.createElement('br'))
   var text = document.createElement('textarea')
      text.setAttribute('id','edit_module_text')
      text.style.width = '100%'
      text.style.height= '100%'
      text.value = def
      win.document.body.appendChild(text)
   function reload_module(idnumber) {
      }
   function update_module(idnumber) {
      //
      // save links
      //
      var ins = document.getElementById(
         JSON.stringify({id:idnumber,type:'inputs'}))
      var inlinks = []
      for (var i = 1; i < ins.childNodes.length; ++i) {
         var links = JSON.parse(ins.childNodes[i].dataset.links)
         for (var l in links)
            inlinks.push(links[l])
         }
      var outs = document.getElementById(
         JSON.stringify({id:idnumber,type:'outputs'}))
      var outlinks = []
      for (var i = 1; i < outs.childNodes.length; ++i) {
         var links = JSON.parse(outs.childNodes[i].dataset.links)
         for (var l in links)
            outlinks.push(links[l])
         }
      //
      // delete module
      //
      delete_module(idnumber)
      //
      // add module
      //
      var def = text.value
      try {
         eval('var args = '+def)
         }
      catch (err) {
         console.log(err.message)
         return
         }
      args.definition = def
      args.id = idnumber
      args.top = top
      args.left = left
      args.filename = filename
      add_module(args)
      //
      // add links
      //
      for (var l in inlinks) {
         eval('var link = '+inlinks[l])
         eval('var linksrc = '+link.source)
         eval('var linkdst = '+link.dest)
         var src = document.getElementById(
            JSON.stringify(
               {id:linksrc.id,type:linksrc.type,name:linksrc.name}))
         var dst = document.getElementById(
            JSON.stringify(
               {id:linkdst.id,type:linkdst.type,name:linkdst.name}))
         add_link(src,dst)
         }
      for (var l in outlinks) {
         eval('var link = '+outlinks[l])
         eval('var linksrc = '+link.source)
         eval('var linkdst = '+link.dest)
         var src = document.getElementById(
            JSON.stringify(
               {id:linksrc.id,type:linksrc.type,name:linksrc.name}))
         var dst = document.getElementById(
            JSON.stringify(
               {id:linkdst.id,type:linkdst.type,name:linkdst.name}))
         add_link(src,dst)
         }
      }
   }
//
// UI routines
//
function set_prompt(txt) {
   var span = document.getElementById('prompt')
   span.childNodes[0].innerHTML = ' '+txt
   }
function get_prompt(txt,fn) {
   var div = document.getElementById('prompt')
   if (div.childNodes.length > 2)
      //
      // already getting a prompt
      //
      return
   var text = document.createElement('input')
      text.type = 'text'
      text.size = 20
      text.value = txt
      text.addEventListener('keydown',function(evt){
         if (evt.key == 'Enter') {
            var div = document.getElementById('prompt')
            div.removeChild(div.childNodes[1])
            div.removeChild(div.childNodes[1])
            set_prompt('')
            fn(text.value)
            }
         })
      div.appendChild(text)
      div.appendChild(document.createTextNode(' (enter)'))
      text.focus()
   }
function nothing(evt) {
   evt.preventDefault()
   evt.stopPropagation()
   }
//
// link routines
//
mods.add_link = function(src,dst) {
   add_link(src,dst)
   }
function add_link(src,dst) {
   //
   // link order from out to in
   //
   if (src.id.indexOf('outputs') == -1) {
      var tmp = src
      src = dst
      dst = tmp
      }
   //
   // check if link exists
   //
   var id = JSON.stringify({source:src.id,dest:dst.id})
   var link = document.getElementById(id)
   if (link != null) {
      //
      // yes, remove it and return
      //
      delete_link(id)
      return
      }
   //
   // no, add link
   //
   var links = JSON.parse(src.dataset.links)
      links.push(id)
      src.dataset.links = JSON.stringify(links)
   var links = JSON.parse(dst.dataset.links)
      links.push(id)
      dst.dataset.links = JSON.stringify(links)
   //
   // draw link
   //
   xsrc = src.parentNode.parentNode.offsetLeft
      +parseFloat(src.dataset.dx)
   ysrc = src.parentNode.parentNode.offsetTop
      +parseFloat(src.dataset.dy)
      -mods.ui.header
   xdst = dst.parentNode.parentNode.offsetLeft
      +parseFloat(dst.dataset.dx)
   ydst = dst.parentNode.parentNode.offsetTop
      +parseFloat(dst.dataset.dy)
      -mods.ui.header
   var links = document.getElementById('links')
   var path = document.createElementNS('http://www.w3.org/2000/svg','path')
      path.setAttribute('id',id)
      path.setAttribute('d','M'+xsrc+','+ysrc+' C'+(xsrc+mods.ui.bezier)+','
         +ysrc+' '+(xdst-mods.ui.bezier)+','+ydst+' '+xdst+','+ydst)
      path.setAttribute('fill','none')
      path.setAttribute('stroke','rgb(0,0,128)')
      path.setAttribute('stroke-width',3)
      links.appendChild(path)
   /*
   //
   // don't trigger link
   //
   eval('var evtid = '+src.id)
   evtid.type = 'output'
   var evtstr = JSON.stringify(evtid)
   var evt = new CustomEvent(evtstr)
   window.dispatchEvent(evt)
   */
   }
function delete_link(linkid) {
   //
   // delete a link
   //
   var links = document.getElementById('links')
   links.removeChild(document.getElementById(linkid))
   link = JSON.parse(linkid)
   src = document.getElementById(link.source)
   dst = document.getElementById(link.dest)
   var links = JSON.parse(src.dataset.links)
      var index = links.indexOf(linkid)
      links.splice(index,1)
      src.dataset.links = JSON.stringify(links)
   var links = JSON.parse(dst.dataset.links)
      var index = links.indexOf(linkid)
      links.splice(index,1)
      dst.dataset.links = JSON.stringify(links)
   }
function draw_links(idnumber,color) {
   //
   // draw a module's links
   //
   var ins = document.getElementById(
      JSON.stringify({id:idnumber,type:'inputs'}))
   var outs = document.getElementById(
      JSON.stringify({id:idnumber,type:'outputs'}))
   for (var i = 1; i < ins.childNodes.length; ++i) {
      var links = JSON.parse(ins.childNodes[i].dataset.links)
      for (var l in links)
         draw_link(links[l],color)
      }
   for (var i = 1; i < outs.childNodes.length; ++i) {
      var links = JSON.parse(outs.childNodes[i].dataset.links)
      for (var l in links)
         draw_link(links[l],color)
      }
   }
function draw_link(id,color) {
   //
   // draw a link
   //
   var link = JSON.parse(id)
   src = document.getElementById(link.source)
   dst = document.getElementById(link.dest)
   var path = document.getElementById(id)
   xsrc = src.parentNode.parentNode.offsetLeft
      +parseFloat(src.dataset.dx)
   ysrc = src.parentNode.parentNode.offsetTop
      +parseFloat(src.dataset.dy)
      -mods.ui.header
   xdst = dst.parentNode.parentNode.offsetLeft
      +parseFloat(dst.dataset.dx)
   ydst = dst.parentNode.parentNode.offsetTop
      +parseFloat(dst.dataset.dy)
      -mods.ui.header
   path.setAttribute('d','M'+xsrc+','+ysrc+' C'+(xsrc+mods.ui.bezier)+','
      +ysrc+' '+(xdst-mods.ui.bezier)+','+ydst+' '+xdst+','+ydst)
   path.setAttribute('stroke',color)
   }
//
// mods routines to be called from modules
//
mods.fit = function(div) {
   //
   // fit a module
   //
   div.style.left = div.dataset.divNameSize/2-div.clientWidth/2
   var divin = document.getElementById(
      JSON.stringify(
         {id:div.dataset.id,type:'inputs'}))
   divin.style.left = div.dataset.divNameSize/2-div.clientWidth/2-divin.clientWidth
   var divout = document.getElementById(
      JSON.stringify(
         {id:div.dataset.id,type:'outputs'}))
   divout.style.left = div.dataset.divNameSize/2+div.clientWidth/2
   }
mods.output = function(mod,varname,val) {
   //
   // send module outputs
   //
   var div = mod.div
   var key = JSON.parse(div.id)
   var idnumber = key.id
   var out = document.getElementById(
      JSON.stringify(
         {id:idnumber,type:'outputs',name:varname}))
   var links = JSON.parse(out.dataset.links)
   for (var l in links) {
      var link = JSON.parse(links[l])
      var dest = JSON.parse(link.dest)
      var divin = document.getElementById(JSON.stringify(
         {id:dest.id,type:'inputs',name:dest.name}))
      var evt = new CustomEvent(divin.dataset.id,{detail:val})
      window.dispatchEvent(evt)
      }
   }
//
// module mod-ification calls
//
mods.module_create = function(args) {
   var event = {target:{result:args}}
   var div = mod_load_handler(event)
   return(div)
   }
mods.module_delete = function(id) {
   delete_module(id)
   }
mods.module_id = function(div) {
   return div.parentNode.id
   }
mods.module_left = function(id) {
   var module = document.getElementById(id)
   return (parseInt(module.style.left))
   }
mods.module_move = function(id,dx,dy) {
   var module = document.getElementById(id)
   var top = parseInt(module.style.top)
   module.style.top = top+dy
   module.dataset.top = top+dy
   var left = parseInt(module.style.left)
   module.style.left = left+dx
   module.dataset.left = left+dx
   draw_links(id,mods.ui.link_color)
   }
mods.module_inputs = function(id,index) {
   var module = document.getElementById(id)
   var inputs = document.getElementById(
      JSON.stringify({id:id,type:'inputs'}))
   console.log(inputs.childNodes[index])
   }
mods.module_outputs = function(id,index) {
   var module = document.getElementById(id)
   var outputs = document.getElementById(
      JSON.stringify({id:id,type:'outputs'}))
   console.log(outputs.childNodes[index])
   }
mods.module_position = function(id,x,y) {
   var module = document.getElementById(id)
   var top = parseInt(module.style.top)
   module.style.top = y
   module.dataset.top = y
   var left = parseInt(module.style.left)
   module.style.left = x
   module.dataset.left = x
   draw_links(id,mods.ui.link_color)
   }
mods.module_top = function(id) {
   var module = document.getElementById(id)
   return (parseInt(module.style.top))
   }
mods.module_width = function(id) {
   var module = document.getElementById(id)
   return (parseInt(module.clientWidth))
   }
//
// input event handlers
//
function input_over(evt) {
   evt.target.style.fontWeight = 'bold'
   var links = JSON.parse(evt.target.dataset.links)
   for (var l in links)
      draw_link(links[l],mods.ui.link_highlight)
   if (mods.ui.source == null)
      set_prompt('click to link')
   }
function input_out(evt) {
   evt.target.style.fontWeight = 'normal'
   var links = JSON.parse(evt.target.dataset.links)
   for (var l in links)
      draw_link(links[l],mods.ui.link_color)
   if (mods.ui.source == null)
      set_prompt('')
   }
function input_mousedown(evt) {
   if (mods.ui.source == null) {
      mods.ui.source = evt.target
      set_prompt('variable to link/unlink to?')
      }
   else {
      add_link(mods.ui.source,evt.target)
      set_prompt('')
      mods.ui.source = null
      }
   }
function input_touchdown(evt) {
   if (mods.ui.source == null) {
      mods.ui.source = evt.target
      set_prompt('variable to link/unlink to?')
      }
   else {
      add_link(mods.ui.source,evt.target)
      set_prompt('')
      mods.ui.source = null
      }
   }
//
// output event handlers
//
function output_over(evt) {
   evt.target.style.fontWeight = 'bold'
   var links = JSON.parse(evt.target.dataset.links)
   for (var l in links)
      draw_link(links[l],mods.ui.link_highlight)
   if (mods.ui.source == null)
      set_prompt('click to link')
   }
function output_out(evt) {
   evt.target.style.fontWeight = 'normal'
   var links = JSON.parse(evt.target.dataset.links)
   for (var l in links)
      draw_link(links[l],mods.ui.link_color)
   if (mods.ui.source == null)
      set_prompt('')
   }
function output_mousedown(evt) {
   if (mods.ui.source == null) {
      mods.ui.source = evt.target
      set_prompt('variable to link/unlink to?')
      }
   else {
      add_link(mods.ui.source,evt.target)
      set_prompt('')
      mods.ui.source = null
      }
   }
function output_touchdown(evt) {
   if (mods.ui.source == null) {
      mods.ui.source = evt.target
      set_prompt('variable to link/unlink to?')
      }
   else {
      add_link(mods.ui.source,evt.target)
      set_prompt('')
      mods.ui.source = null
      }
   }
//
// name event handlers
//
function name_over(evt) {
   evt.target.style.fontWeight = 'bold'
   if (mods.ui.source == null)
      set_prompt('click and drag to move')
   }
function name_out(evt) {
   evt.target.style.fontWeight = 'normal'
   if (mods.ui.source == null)
      set_prompt('')
   }
function name_mousedown(evt) {
   evt.preventDefault()
   evt.stopPropagation()
   var div = document.getElementById(evt.target.parentNode.id)
      div.style.zIndex = 1
      mods.ui.xstart = evt.clientX
      mods.ui.ystart = evt.clientY
      mods.ui.selected[evt.target.parentNode.id] = true
      window.addEventListener('mousemove',name_mousemove)
      window.addEventListener('mouseup',name_mouseup)
   }
function name_mousemove(evt) {
   evt.preventDefault()
   evt.stopPropagation()
   var t = mods_transform()
   for (var id in mods.ui.selected) {
      var div = document.getElementById(id)
         var dx = (evt.clientX-mods.ui.xstart)/t.s
         var dy = (evt.clientY-mods.ui.ystart)/t.s
         var newleft = parseFloat(div.dataset.left)+dx
         var newtop = parseFloat(div.dataset.top)+dy
         div.style.left = newleft+'px'
         div.style.top = newtop+'px'
      draw_links(id,mods.ui.link_color)
      }
   }
function name_mouseup(evt) {
   evt.preventDefault()
   evt.stopPropagation()
   var t = mods_transform()
   for (var id in mods.ui.selected) {
      var div = document.getElementById(id)
         div.style.zIndex = 0
         div.childNodes[0].style.fontWeight = 'normal'
         var dx = (evt.clientX-mods.ui.xstart)/t.s
         var dy = (evt.clientY-mods.ui.ystart)/t.s
         div.dataset.left = parseFloat(div.dataset.left)+dx
         div.dataset.top = parseFloat(div.dataset.top)+dy
         window.removeEventListener('mousemove',name_mousemove)
         window.removeEventListener('mouseup',name_mouseup)
      }
   mods.ui.selected = {}
   }
function name_touchdown(evt) {
   evt.preventDefault()
   evt.stopPropagation()
   var div = document.getElementById(evt.target.parentNode.id)
      div.style.zIndex = 1
      mods.ui.xstart = evt.changedTouches[0].pageX
      mods.ui.ystart = evt.changedTouches[0].pageY
      mods.ui.selected[evt.target.parentNode.id] = true
      window.addEventListener('touchmove',name_touchmove)
      window.addEventListener('touchend',name_touchup)
   }
function name_touchmove(evt) {
   evt.preventDefault()
   evt.stopPropagation()
   var t = mods_transform()
   for (var id in mods.ui.selected) {
      var div = document.getElementById(id)
         var dx = (evt.changedTouches[0].pageX-mods.ui.xstart)/t.s
         var dy = (evt.changedTouches[0].pageY-mods.ui.ystart)/t.s
         var newleft = parseFloat(div.dataset.left)+dx
         var newtop = parseFloat(div.dataset.top)+dy
         div.style.left = newleft+'px'
         div.style.top = newtop+'px'
      draw_links(id,mods.ui.link_color)
      }
   }
function name_touchup(evt) {
   evt.preventDefault()
   evt.stopPropagation()
   var t = mods_transform()
   for (var id in mods.ui.selected) {
      var div = document.getElementById(id)
         div.style.zIndex = 0
         var dx = (evt.changedTouches[0].pageX-mods.ui.xstart)/t.s
         var dy = (evt.changedTouches[0].pageY-mods.ui.ystart)/t.s
         div.dataset.left = parseFloat(div.dataset.left)+dx
         div.dataset.top = parseFloat(div.dataset.top)+dy
         window.removeEventListener('touchmove',name_touchmove)
         window.removeEventListener('touchend',name_touchup)
      }
   mods.ui.selected = {}
   }
})()