svg.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435
  1. "use strict";
  2. FW_version["svg.js"] = "$Id: svg.js 17699 2018-11-07 11:04:58Z rudolfkoenig $";
  3. var svgCallback={};
  4. if(!svgNS) {
  5. var svgNS = "http://www.w3.org/2000/svg";
  6. var svg_initialized={}, lastHidden;
  7. var svg_db, svg_dbtbl = "SVG_KEYVALUE", svg_pastedata, svgCounter=0;
  8. }
  9. function
  10. svg_initDb(nextFn)
  11. {
  12. if(window.indexedDB == undefined)
  13. return;
  14. var dbreq = indexedDB.open("FHEM", 1);
  15. dbreq.onsuccess = function(op) { svg_db = op.target.result; nextFn() }
  16. dbreq.onerror = function(op) {
  17. var oldfn = window.onerror;
  18. window.onerror = undefined; // stupid FireFox private mode (Forum #64541)
  19. log("indexedDB.open Error: " + op.message);
  20. setTimeout(function(){window.onerror = oldfn;}, 100);
  21. }
  22. dbreq.onupgradeneeded = function(op) {
  23. svg_db = op.target.result;
  24. svg_db.createObjectStore(svg_dbtbl, { keyPath:"key" });
  25. };
  26. }
  27. function
  28. svg_save(key, value)
  29. {
  30. if(!svg_db)
  31. return;
  32. var os = svg_db.transaction([svg_dbtbl],"readwrite")
  33. .objectStore(svg_dbtbl);
  34. os.put({key:key, val:value});
  35. }
  36. function
  37. svg_load(key, nextFn)
  38. {
  39. if(!svg_db)
  40. return;
  41. var req = svg_db.transaction([svg_dbtbl],"readonly")
  42. .objectStore(svg_dbtbl)
  43. .get(key);
  44. req.onsuccess = function(e) { if(req.result) nextFn(req.result.val); }
  45. }
  46. function
  47. svg_prepareHash(el)
  48. {
  49. var obj = { y_mul:0,y_h:0,y_min:0, decimals:0,
  50. t_mul:0,x_off:0,x_min:0, x_mul:0, log_scale:undefined };
  51. for(var name in obj) {
  52. var n = $(el).attr(name);
  53. if(n)
  54. obj[name] = parseFloat(n);
  55. }
  56. return obj;
  57. }
  58. function
  59. svg_click(evt)
  60. {
  61. var t = evt.target;
  62. var o = svg_prepareHash(t);
  63. var svg=$(t).closest("svg"), x=evt.clientX, y=evt.clientY;
  64. if($(svg).parent().length) { // isEmbed=0
  65. var off = $(svg).offset();
  66. x -= off.left;
  67. y -= off.top;
  68. }
  69. var y_org = (((o.y_h-y)/o.y_mul)+o.y_min).toFixed(o.decimals);
  70. var d = new Date((((x-o.x_min)/o.t_mul)+o.x_off) * 1000);
  71. var ts = (d.getHours() < 10 ? '0' : '') + d.getHours() + ":"+
  72. (d.getMinutes() < 10 ? '0' : '') + d.getMinutes();
  73. var tl = $(t).closest("svg").find("#svg_title");
  74. $(tl).html($(t).attr("title")+": "+y_org+" ("+ts+")");
  75. }
  76. function
  77. sv_menu(evt, embed)
  78. {
  79. var label = evt.target;
  80. var svg = $(label).closest("svg");
  81. var svgNode = $(svg).get(0);
  82. var lid = $(label).attr("line_id");
  83. var sel = $(svg).find("#"+lid);
  84. var selNode = $(sel).get(0);
  85. // horizintalLine* consists of a list of lines, too much work do handle
  86. // it.
  87. if(!selNode)
  88. return;
  89. var tl = $(svg).find("#svg_title");
  90. var par = svgNode.par;
  91. var lines = $(svg).find("[line_id]");
  92. var hidden = $(svg).find(".hidden");
  93. function
  94. myPathSegList(node) // chrome 48+ removed the pathSegList interface
  95. {
  96. this.arr = $(node).attr("d").split(/ */);
  97. this.arr.splice(0,1); // remove M
  98. this.arr.splice(1,1); // remove L/Q/etc
  99. this.numberOfItems = this.arr.length;
  100. this.getItem = function(pos)
  101. {
  102. var xy = this.arr[pos].split(",");
  103. return { x:parseFloat(xy[0]), y:parseFloat(xy[1]) };
  104. }
  105. }
  106. function
  107. showValOff() {
  108. $(svg).find("[id]").each(function(){delete($(this).get(0).showVal)});
  109. $(svg).off("mousemove");
  110. if(par && par.circle) {
  111. $(par.circle).remove();
  112. $(par.div).remove();
  113. }
  114. }
  115. var sn = selNode.nodeName,
  116. pn = (sn=="path" ? "d" : "points"),
  117. arrName = (sn=="path" ? "pathSegList" : "points");
  118. FW_menu(evt, label,
  119. ["Copy", "Paste",
  120. selNode.isHidden ? "Show line" : "Hide line",
  121. "Hide other lines",
  122. "Show all lines",
  123. selNode.showVal ? "Stop displaying values" : "Display plot values" ],
  124. [undefined, svg_pastedata == undefined,
  125. !selNode.isHidden && (lines.length - hidden.length) == 1,
  126. !selNode.isHidden && (lines.length - hidden.length) == 1,
  127. hidden.length==0,
  128. selNode.isHidden || (sn!="polyline" && sn!="path") ],
  129. function(arg) {
  130. //////////////////////////////////// copy
  131. if(arg == 0) {
  132. svg_pastedata = {
  133. key:"svg_pastedata",
  134. tag:sn, attr:pn,
  135. y_min:$(sel).attr("y_min"),
  136. y_mul:$(sel).attr("y_mul"),
  137. datapoints:$(sel).attr(pn)
  138. };
  139. svg_save("svg_pastedata", svg_pastedata);
  140. }
  141. //////////////////////////////////// paste
  142. if(arg == 1) {
  143. var doc = $(svg).get(0).ownerDocument;
  144. var o=doc.createElementNS(svgNS, svg_pastedata.tag);
  145. o.setAttribute("class", "SVGplot pasted");
  146. o.setAttribute(svg_pastedata.attr, svg_pastedata.datapoints);
  147. var h = parseFloat($(sel).attr("y_h"));
  148. var ny_mul = parseFloat(svg_pastedata.y_mul);
  149. var ny_min = parseInt(svg_pastedata.y_min);
  150. var y_mul = parseFloat($(sel).attr("y_mul"));
  151. var y_min = parseInt($(sel).attr("y_min"));
  152. var tr =
  153. "translate(0,"+ (h/y_mul+y_min-h/ny_mul-ny_min)*y_mul +") "+
  154. "scale(1, "+ (y_mul/ny_mul) +") ";
  155. o.setAttribute("transform", tr);
  156. svgNode.appendChild(o);
  157. }
  158. //show/hide
  159. if(arg == 2) {
  160. setVisibility(lid, selNode.isHidden?1:0);
  161. $(label).attr("opacity", selNode.isHidden?0.4:1);
  162. }
  163. //hide other
  164. if(arg == 3) {
  165. $(svg).find("[id]").each(function(){
  166. var id = $(this).attr("id");
  167. if(id.indexOf("line_") != 0 )
  168. return;
  169. var label = $(svg).find('[line_id="'+id+'"]');
  170. if( !label.length ) // ignore lines with label none
  171. return;
  172. var sel = $(svg).find("#"+id);
  173. var selNode = $(sel).get(0);
  174. if( (selNode.isHidden?false:true) != (id == lid) )
  175. setVisibility(id, id == lid);
  176. label.attr("opacity", id == lid?1:0.4);
  177. } );
  178. }
  179. //show all
  180. if(arg == 4) {
  181. $(svg).find("[line_id]").attr("opacity",1);
  182. $(svg).find("[id]").each(function(){
  183. var id = $(this).attr("id");
  184. if(id.indexOf("line_") != 0 )
  185. return;
  186. var sel = $(svg).find("#"+id);
  187. var selNode = $(sel).get(0);
  188. if( !selNode.isHidden )
  189. return;
  190. setVisibility(id, 1);
  191. //$(svg).find('[line_id="'+id+'"]').attr("opacity",1);
  192. } );
  193. }
  194. //////////////////////////////////// value display
  195. if(arg == 5) {
  196. var hadShowVal = selNode.showVal;
  197. showValOff();
  198. if(!hadShowVal) {
  199. selNode.showVal = true;
  200. $(svg).mousemove(mousemove);
  201. svgNode.par = par = svg_prepareHash(selNode);
  202. par.circle =
  203. $(svg).get(0).ownerDocument.createElementNS(svgNS, "circle");
  204. $(par.circle).attr("id", "svgmarker").attr("r", "8");
  205. $(svg).append(par.circle);
  206. par.div = $('<div id="svgmarker">');
  207. par.divoffY = $(embed ? embed : svg).offset().top -
  208. $("#content").offset().top-50 +
  209. $("#content").scrollTop();
  210. var parent = (embed ? $(embed).parent() : $(svg).parent());
  211. $(parent).append(par.div);
  212. var pl = selNode[arrName];
  213. if(!pl)
  214. selNode[arrName] = pl = new myPathSegList(selNode);
  215. if(pl.numberOfItems > 2)
  216. mousemove({pageX:pl.getItem(pl.numberOfItems-2).x});
  217. }
  218. }
  219. if( arg >= 2 && arg <= 4 ) {
  220. var hidden = $(svg).find(".hidden");
  221. if( lines.length - hidden.length == 1 ) {
  222. $(tl).attr("hiddentitle", $(tl).text());
  223. if($(sel).attr(pn) != null)
  224. $(tl).text($(label).attr("title"));
  225. } else if( $(tl).attr("hiddentitle") ) {
  226. $(tl).text($(tl).attr("hiddentitle"));
  227. $(tl).removeAttr("hiddentitle")
  228. }
  229. }
  230. }, embed);
  231. function pad0(v) { return (v < 10 ? '0'+v :v); }
  232. function
  233. mousemove(e)
  234. {
  235. var xRaw = e.pageX, pl = selNode[arrName], l = pl.numberOfItems, i1;
  236. if(!embed)
  237. xRaw -= $(svg).offset().left;
  238. for(i1=0; i1<l; i1++)
  239. if(pl.getItem(i1).x > xRaw)
  240. break;
  241. if(i1==l || i1==0)
  242. return;
  243. var pp=pl.getItem(i1-1), pn=pl.getItem(i1);
  244. var xR = (xRaw-pp.x)/(pn.x-pp.x); // Compute interim values
  245. var yRaw = pp.y+xR*(pn.y-pp.y);
  246. var y = (((par.y_h-yRaw)/par.y_mul)+par.y_min);
  247. if( par.log_scale ) {
  248. y *= par.log_scale;
  249. y = Math.pow(10,y) - 1;
  250. }
  251. y = y.toFixed(par.decimals);
  252. if( par.x_mul ) {
  253. ts = (((xRaw-par.x_min)/par.x_mul)+par.x_off).toFixed(par.decimals);
  254. } else {
  255. var d = new Date((((xRaw-par.x_min)/par.t_mul)+par.x_off) * 1000), ts;
  256. if(par.t_mul < 0.0001) { // Year
  257. ts =(pad0(d.getMonth()+1))+"."+pad0(d.getDate()+"."+(d.getYear()+1900));
  258. } else if(par.t_mul < 0.001) { // Month
  259. ts = (pad0(d.getMonth()+1))+"."+pad0(d.getDate())+
  260. ". "+pad0(d.getHours())+":"+pad0(d.getMinutes());
  261. } else {
  262. ts = pad0(d.getHours())+":"+pad0(d.getMinutes());
  263. }
  264. }
  265. $(par.circle).attr("cx", xRaw).attr("cy", yRaw);
  266. var yd = Math.floor((yRaw+par.divoffY) / 20)*20;
  267. $(par.div).html(ts+" "+y)
  268. .css({ left:xRaw-20, top:yd });
  269. }
  270. function
  271. setVisibility(id,visible)
  272. {
  273. var sel = $(svg).find("#"+id);
  274. var selNode = $(sel).get(0);
  275. var currval = visible?1:0;
  276. var h = parseFloat(sel.attr("y_h"));
  277. //sel.attr("transform", "translate(0,"+h*(1-currval)+") "
  278. //+ "scale(1,"+currval+")");
  279. if( !visible
  280. && selNode.showVal )
  281. showValOff();
  282. animateVisibility(sel,visible?0:1, visible?1:0);
  283. if( visible ) {
  284. delete(selNode.isHidden);
  285. sel.attr("class", sel.attr("class").replace(/ hidden/, "" ) );
  286. } else {
  287. selNode.isHidden = true;
  288. sel.attr("class", sel.attr("class") + " hidden" );
  289. }
  290. }
  291. function
  292. animateVisibility(sel, currval, maxval)
  293. {
  294. var h = parseFloat(sel.attr("y_h"));
  295. sel.attr("transform", "translate(0,"+h*(1-currval)+") "+
  296. "scale(1,"+currval+")");
  297. if(currval != maxval) {
  298. currval += (currval<maxval ? 0.02 : -0.02);
  299. currval = Math.round(currval*100)/100;
  300. setTimeout(function(){ animateVisibility(sel,currval,maxval) }, 10);
  301. }
  302. }
  303. }
  304. function
  305. svg_init_one(embed, svg)
  306. {
  307. var sid = $(svg).attr("id");
  308. if(svg_initialized[sid])
  309. return;
  310. svg_initialized[sid] = true;
  311. $("text.legend", svg).click(function(e){sv_menu(e, embed)});
  312. for(var i in svgCallback)
  313. svgCallback[i](svg);
  314. }
  315. function
  316. svg_init(par) // also called directly from perl, in race condition
  317. {
  318. $("embed").each(function(){
  319. var e = this;
  320. var src = $(e).attr("src");
  321. var ed = FW_getSVG(e);
  322. if(!src || src.indexOf("SVG_showLog") < 0 || !ed)
  323. return;
  324. var sTag = $("svg", ed)[0]; // "not well-formed" warning in FireFox
  325. if((par && $(sTag).attr("id") != par))
  326. return;
  327. svg_init_one(e, sTag);
  328. });
  329. }
  330. $(document).ready(function(){
  331. if(svgCounter++ > 0) // if svg.js is included twice, e.g. by Dashboard
  332. return;
  333. svg_init(); // <embed><svg>
  334. svg_initDb(function(){
  335. svg_load("svg_pastedata", function(val) {svg_pastedata = val} );
  336. });
  337. $("svg[id]").each(function(){ // <svg> (direct)
  338. if($(this).attr("id").indexOf("SVGPLOT") == 0)
  339. svg_init_one(undefined, this);
  340. });
  341. });
  342. // longpollSVG code below
  343. function
  344. FW_svgUpdateDevs(devs)
  345. {
  346. // if matches, refresh the SVG by removing and readding the embed tag
  347. var embArr = document.getElementsByTagName("embed");
  348. for(var i = 0; i < embArr.length; i++) {
  349. var svg = FW_getSVG(embArr[i]);
  350. if(!svg || !svg.firstChild || !svg.firstChild.nextSibling)
  351. continue;
  352. if(svg.contentType != "image/svg+xml" &&
  353. typeof embArr[i].getSVGDocument != "function")
  354. continue;
  355. svg = svg.firstChild.nextSibling;
  356. var flog = svg.getAttribute("flog");
  357. if(!flog)
  358. continue;
  359. flog = flog.replace(/\\x3a/g, ".");
  360. log("longpollSVG filter:"+flog);
  361. for(var j=0; j < devs.length; j++) {
  362. var d = devs[j];
  363. var ev = d[0]+":"+d[1];
  364. if(ev.match(flog)) {
  365. log("longpollSVG: reload SVG");
  366. delete(svg_initialized[$(svg).attr("id")]);
  367. var e = embArr[i];
  368. var newE = document.createElement("embed");
  369. for(var k=0; k<e.attributes.length; k++)
  370. newE.setAttribute(e.attributes[k].name, e.attributes[k].value);
  371. e.parentNode.insertBefore(newE, e);
  372. e.parentNode.removeChild(e);
  373. break;
  374. }
  375. }
  376. }
  377. }
  378. FW_widgets.SVG = { updateDevs:FW_svgUpdateDevs };