ext-markers.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580
  1. /*globals svgEditor, svgCanvas, $*/
  2. /*jslint vars: true, eqeq: true, todo: true */
  3. /*
  4. * ext-markers.js
  5. *
  6. * Licensed under the Apache License, Version 2
  7. *
  8. * Copyright(c) 2010 Will Schleter
  9. * based on ext-arrows.js by Copyright(c) 2010 Alexis Deveria
  10. *
  11. * This extension provides for the addition of markers to the either end
  12. * or the middle of a line, polyline, path, polygon.
  13. *
  14. * Markers may be either a graphic or arbitary text
  15. *
  16. * to simplify the coding and make the implementation as robust as possible,
  17. * markers are not shared - every object has its own set of markers.
  18. * this relationship is maintained by a naming convention between the
  19. * ids of the markers and the ids of the object
  20. *
  21. * The following restrictions exist for simplicty of use and programming
  22. * objects and their markers to have the same color
  23. * marker size is fixed
  24. * text marker font, size, and attributes are fixed
  25. * an application specific attribute - se_type - is added to each marker element
  26. * to store the type of marker
  27. *
  28. * TODO:
  29. * remove some of the restrictions above
  30. * add option for keeping text aligned to horizontal
  31. * add support for dimension extension lines
  32. *
  33. */
  34. svgEditor.addExtension("Markers", function(S) {
  35. var // svgcontent = S.svgcontent,
  36. addElem = S.addSvgElementFromJson,
  37. selElems;
  38. var mtypes = ['start','mid','end'];
  39. var marker_prefix = 'se_marker_';
  40. var id_prefix = 'mkr_';
  41. // note - to add additional marker types add them below with a unique id
  42. // and add the associated icon(s) to marker-icons.svg
  43. // the geometry is normallized to a 100x100 box with the origin at lower left
  44. // Safari did not like negative values for low left of viewBox
  45. // remember that the coordinate system has +y downward
  46. var marker_types = {
  47. nomarker: {},
  48. leftarrow:
  49. {element:'path', attr:{d:'M0,50 L100,90 L70,50 L100,10 Z'}},
  50. rightarrow:
  51. {element:'path', attr:{d:'M100,50 L0,90 L30,50 L0,10 Z'}},
  52. textmarker:
  53. {element:'text', attr: {x:0, y:0,'stroke-width':0,'stroke':'none','font-size':75,'font-family':'serif','text-anchor':'left',
  54. 'xml:space': 'preserve'}},
  55. forwardslash:
  56. {element:'path', attr:{d:'M30,100 L70,0'}},
  57. reverseslash:
  58. {element:'path', attr:{d:'M30,0 L70,100'}},
  59. verticalslash:
  60. {element:'path', attr:{d:'M50,0 L50,100'}},
  61. box:
  62. {element:'path', attr:{d:'M20,20 L20,80 L80,80 L80,20 Z'}},
  63. star:
  64. {element:'path', attr:{d:'M10,30 L90,30 L20,90 L50,10 L80,90 Z'}},
  65. xmark:
  66. {element:'path', attr:{d:'M20,80 L80,20 M80,80 L20,20'}},
  67. triangle:
  68. {element:'path', attr:{d:'M10,80 L50,20 L80,80 Z'}},
  69. mcircle:
  70. {element:'circle', attr:{r:30, cx:50, cy:50}}
  71. };
  72. var lang_list = {
  73. "en":[
  74. {id: "start_marker_list", title: "Select start marker type" },
  75. {id: "mid_marker_list", title: "Select mid marker type" },
  76. {id: "end_marker_list", title: "Select end marker type" },
  77. {id: "nomarker", title: "No Marker" },
  78. {id: "leftarrow", title: "Left Arrow" },
  79. {id: "rightarrow", title: "Right Arrow" },
  80. {id: "textmarker", title: "Text Marker" },
  81. {id: "forwardslash", title: "Forward Slash" },
  82. {id: "reverseslash", title: "Reverse Slash" },
  83. {id: "verticalslash", title: "Vertical Slash" },
  84. {id: "box", title: "Box" },
  85. {id: "star", title: "Star" },
  86. {id: "xmark", title: "X" },
  87. {id: "triangle", title: "Triangle" },
  88. {id: "mcircle", title: "Circle" },
  89. {id: "leftarrow_o", title: "Open Left Arrow" },
  90. {id: "rightarrow_o", title: "Open Right Arrow" },
  91. {id: "box_o", title: "Open Box" },
  92. {id: "star_o", title: "Open Star" },
  93. {id: "triangle_o", title: "Open Triangle" },
  94. {id: "mcircle_o", title: "Open Circle" }
  95. ]
  96. };
  97. // duplicate shapes to support unfilled (open) marker types with an _o suffix
  98. $.each(['leftarrow','rightarrow','box','star','mcircle','triangle'],function(i,v) {
  99. marker_types[v+'_o'] = marker_types[v];
  100. });
  101. // elem = a graphic element will have an attribute like marker-start
  102. // attr - marker-start, marker-mid, or marker-end
  103. // returns the marker element that is linked to the graphic element
  104. function getLinked(elem, attr) {
  105. var str = elem.getAttribute(attr);
  106. if(!str) {return null;}
  107. var m = str.match(/\(\#(.*)\)/);
  108. if(!m || m.length !== 2) {
  109. return null;
  110. }
  111. return S.getElem(m[1]);
  112. }
  113. function setIcon(pos,id) {
  114. if (id.substr(0,1) !== '\\') {id = '\\textmarker';}
  115. var ci = '#'+id_prefix+pos+'_'+id.substr(1);
  116. svgEditor.setIcon('#cur_' + pos +'_marker_list', $(ci).children());
  117. $(ci).addClass('current').siblings().removeClass('current');
  118. }
  119. // toggles context tool panel off/on
  120. //sets the controls with the selected element's settings
  121. function showPanel(on) {
  122. $('#marker_panel').toggle(on);
  123. if(on) {
  124. var el = selElems[0];
  125. var val;
  126. var ci;
  127. $.each(mtypes, function(i, pos) {
  128. var m = getLinked(el, "marker-" + pos);
  129. var txtbox = $('#' + pos + '_marker');
  130. if (!m) {
  131. val = '\\nomarker';
  132. ci = val;
  133. txtbox.hide(); // hide text box
  134. } else {
  135. if (!m.attributes.se_type) {return;} // not created by this extension
  136. val = '\\' + m.attributes.se_type.textContent;
  137. ci = val;
  138. if (val === '\\textmarker') {
  139. val = m.lastChild.textContent;
  140. //txtbox.show(); // show text box
  141. } else {
  142. txtbox.hide(); // hide text box
  143. }
  144. }
  145. txtbox.val(val);
  146. setIcon(pos,ci);
  147. });
  148. }
  149. }
  150. function addMarker(id, val) {
  151. var txt_box_bg = '#ffffff';
  152. var txt_box_border = 'none';
  153. var txt_box_stroke_width = 0;
  154. var marker = S.getElem(id);
  155. if (marker) {return;}
  156. if (val == '' || val == '\\nomarker') {return;}
  157. var el = selElems[0];
  158. var color = el.getAttribute('stroke');
  159. //NOTE: Safari didn't like a negative value in viewBox
  160. //so we use a standardized 0 0 100 100
  161. //with 50 50 being mapped to the marker position
  162. var refX = 50;
  163. var refY = 50;
  164. var viewBox = "0 0 100 100";
  165. var markerWidth = 5;
  166. var markerHeight = 5;
  167. var strokeWidth = 10;
  168. var se_type;
  169. if (val.substr(0,1) === '\\') {se_type = val.substr(1);}
  170. else {se_type = 'textmarker';}
  171. if (!marker_types[se_type]) {return;} // an unknown type!
  172. // create a generic marker
  173. marker = addElem({
  174. "element": "marker",
  175. "attr": {
  176. "id": id,
  177. "markerUnits": "strokeWidth",
  178. "orient": "auto",
  179. "style": "pointer-events:none",
  180. "se_type": se_type
  181. }
  182. });
  183. if (se_type != 'textmarker') {
  184. var mel = addElem(marker_types[se_type]);
  185. var fillcolor = color;
  186. if (se_type.substr(-2) === '_o') {fillcolor = 'none';}
  187. mel.setAttribute('fill', fillcolor);
  188. mel.setAttribute('stroke', color);
  189. mel.setAttribute('stroke-width', strokeWidth);
  190. marker.appendChild(mel);
  191. } else {
  192. var text = addElem(marker_types[se_type]);
  193. // have to add text to get bounding box
  194. text.textContent = val;
  195. var tb = text.getBBox();
  196. //alert( tb.x + " " + tb.y + " " + tb.width + " " + tb.height);
  197. var pad = 1;
  198. var bb = tb;
  199. bb.x = 0;
  200. bb.y = 0;
  201. bb.width += pad*2;
  202. bb.height += pad*2;
  203. // shift text according to its size
  204. text.setAttribute('x', pad);
  205. text.setAttribute('y', bb.height - pad - tb.height/4); // kludge?
  206. text.setAttribute('fill',color);
  207. refX = bb.width/2 + pad;
  208. refY = bb.height/2 + pad;
  209. viewBox = bb.x + " " + bb.y + " " + bb.width + " " + bb.height;
  210. markerWidth = bb.width/10;
  211. markerHeight = bb.height/10;
  212. var box = addElem({
  213. "element": "rect",
  214. "attr": {
  215. "x": bb.x,
  216. "y": bb.y,
  217. "width": bb.width,
  218. "height": bb.height,
  219. "fill": txt_box_bg,
  220. "stroke": txt_box_border,
  221. "stroke-width": txt_box_stroke_width
  222. }
  223. });
  224. marker.setAttribute("orient",0);
  225. marker.appendChild(box);
  226. marker.appendChild(text);
  227. }
  228. marker.setAttribute("viewBox",viewBox);
  229. marker.setAttribute("markerWidth", markerWidth);
  230. marker.setAttribute("markerHeight", markerHeight);
  231. marker.setAttribute("refX", refX);
  232. marker.setAttribute("refY", refY);
  233. S.findDefs().appendChild(marker);
  234. return marker;
  235. }
  236. function convertline(elem) {
  237. // this routine came from the connectors extension
  238. // it is needed because midpoint markers don't work with line elements
  239. if (!(elem.tagName === 'line')) {return elem;}
  240. // Convert to polyline to accept mid-arrow
  241. var x1 = Number(elem.getAttribute('x1'));
  242. var x2 = Number(elem.getAttribute('x2'));
  243. var y1 = Number(elem.getAttribute('y1'));
  244. var y2 = Number(elem.getAttribute('y2'));
  245. var id = elem.id;
  246. var mid_pt = (' '+((x1+x2)/2)+','+((y1+y2)/2) + ' ');
  247. var pline = addElem({
  248. "element": "polyline",
  249. "attr": {
  250. "points": (x1+','+y1+ mid_pt +x2+','+y2),
  251. "stroke": elem.getAttribute('stroke'),
  252. "stroke-width": elem.getAttribute('stroke-width'),
  253. "fill": "none",
  254. "opacity": elem.getAttribute('opacity') || 1
  255. }
  256. });
  257. $.each(mtypes, function(i, pos) { // get any existing marker definitions
  258. var nam = 'marker-'+pos;
  259. var m = elem.getAttribute(nam);
  260. if (m) {pline.setAttribute(nam,elem.getAttribute(nam));}
  261. });
  262. var batchCmd = new S.BatchCommand();
  263. batchCmd.addSubCommand(new S.RemoveElementCommand(elem, elem.parentNode));
  264. batchCmd.addSubCommand(new S.InsertElementCommand(pline));
  265. $(elem).after(pline).remove();
  266. svgCanvas.clearSelection();
  267. pline.id = id;
  268. svgCanvas.addToSelection([pline]);
  269. S.addCommandToHistory(batchCmd);
  270. return pline;
  271. }
  272. function setMarker() {
  273. var poslist={'start_marker':'start','mid_marker':'mid','end_marker':'end'};
  274. var pos = poslist[this.id];
  275. var marker_name = 'marker-'+pos;
  276. var val = this.value;
  277. var el = selElems[0];
  278. var marker = getLinked(el, marker_name);
  279. if (marker) {$(marker).remove();}
  280. el.removeAttribute(marker_name);
  281. if (val == '') {val = '\\nomarker';}
  282. if (val == '\\nomarker') {
  283. setIcon(pos,val);
  284. S.call("changed", selElems);
  285. return;
  286. }
  287. // Set marker on element
  288. var id = marker_prefix + pos + '_' + el.id;
  289. addMarker(id, val);
  290. svgCanvas.changeSelectedAttribute(marker_name, "url(#" + id + ")");
  291. if (el.tagName === 'line' && pos == 'mid') {el = convertline(el);}
  292. S.call("changed", selElems);
  293. setIcon(pos,val);
  294. }
  295. // called when the main system modifies an object
  296. // this routine changes the associated markers to be the same color
  297. function colorChanged(elem) {
  298. var color = elem.getAttribute('stroke');
  299. $.each(mtypes, function(i, pos) {
  300. var marker = getLinked(elem, 'marker-'+pos);
  301. if (!marker) {return;}
  302. if (!marker.attributes.se_type) {return;} // not created by this extension
  303. var ch = marker.lastElementChild;
  304. if (!ch) {return;}
  305. var curfill = ch.getAttribute('fill');
  306. var curstroke = ch.getAttribute('stroke');
  307. if (curfill && curfill != 'none') {ch.setAttribute('fill', color);}
  308. if (curstroke && curstroke != 'none') {ch.setAttribute('stroke', color);}
  309. });
  310. }
  311. // called when the main system creates or modifies an object
  312. // primary purpose is create new markers for cloned objects
  313. function updateReferences(el) {
  314. $.each(mtypes, function (i, pos) {
  315. var id = marker_prefix + pos + '_' + el.id;
  316. var marker_name = 'marker-'+pos;
  317. var marker = getLinked(el, marker_name);
  318. if (!marker || !marker.attributes.se_type) {return;} // not created by this extension
  319. var url = el.getAttribute(marker_name);
  320. if (url) {
  321. var len = el.id.length;
  322. var linkid = url.substr(-len-1, len);
  323. if (el.id != linkid) {
  324. var val = $('#'+pos + '_marker').attr('value');
  325. addMarker(id, val);
  326. svgCanvas.changeSelectedAttribute(marker_name, "url(#" + id + ")");
  327. if (el.tagName === 'line' && pos == 'mid') {el = convertline(el);}
  328. S.call("changed", selElems);
  329. }
  330. }
  331. });
  332. }
  333. // simulate a change event a text box that stores the current element's marker type
  334. function triggerTextEntry(pos, val) {
  335. $('#'+pos+'_marker').val(val);
  336. $('#'+pos+'_marker').change();
  337. // var txtbox = $('#'+pos+'_marker');
  338. // if (val.substr(0,1)=='\\') {txtbox.hide();}
  339. // else {txtbox.show();}
  340. }
  341. function showTextPrompt(pos) {
  342. var def = $('#'+pos+'_marker').val();
  343. if (def.substr(0,1) === '\\') {def = '';}
  344. $.prompt('Enter text for ' + pos + ' marker', def , function(txt) {
  345. if (txt) {triggerTextEntry(pos, txt);}
  346. });
  347. }
  348. /*
  349. function setMarkerSet(obj) {
  350. var parts = this.id.split('_');
  351. var set = parts[2];
  352. switch (set) {
  353. case 'off':
  354. triggerTextEntry('start','\\nomarker');
  355. triggerTextEntry('mid','\\nomarker');
  356. triggerTextEntry('end','\\nomarker');
  357. break;
  358. case 'dimension':
  359. triggerTextEntry('start','\\leftarrow');
  360. triggerTextEntry('end','\\rightarrow');
  361. showTextPrompt('mid');
  362. break;
  363. case 'label':
  364. triggerTextEntry('mid','\\nomarker');
  365. triggerTextEntry('end','\\rightarrow');
  366. showTextPrompt('start');
  367. break;
  368. }
  369. }
  370. */
  371. // callback function for a toolbar button click
  372. function setArrowFromButton(obj) {
  373. var parts = this.id.split('_');
  374. var pos = parts[1];
  375. var val = parts[2];
  376. if (parts[3]) { val += '_' + parts[3];}
  377. if (val != 'textmarker') {
  378. triggerTextEntry(pos, '\\'+val);
  379. } else {
  380. showTextPrompt(pos);
  381. }
  382. }
  383. function getTitle(lang, id) {
  384. var i, list = lang_list[lang];
  385. for (i in list) {
  386. if (list.hasOwnProperty(i) && list[i].id == id) {
  387. return list[i].title;
  388. }
  389. }
  390. return id;
  391. }
  392. // build the toolbar button array from the marker definitions
  393. // TODO: need to incorporate language specific titles
  394. function buildButtonList() {
  395. var buttons=[];
  396. // var i = 0;
  397. /*
  398. buttons.push({
  399. id:id_prefix + 'markers_off',
  400. title:'Turn off all markers',
  401. type:'context',
  402. events: { 'click': setMarkerSet },
  403. panel: 'marker_panel'
  404. });
  405. buttons.push({
  406. id:id_prefix + 'markers_dimension',
  407. title:'Dimension',
  408. type:'context',
  409. events: { 'click': setMarkerSet },
  410. panel: 'marker_panel'
  411. });
  412. buttons.push({
  413. id:id_prefix + 'markers_label',
  414. title:'Label',
  415. type:'context',
  416. events: { 'click': setMarkerSet },
  417. panel: 'marker_panel'
  418. });
  419. */
  420. $.each(mtypes, function (k, pos) {
  421. var listname = pos + "_marker_list";
  422. var def = true;
  423. $.each(marker_types, function (id, v) {
  424. var title = getTitle('en', id);
  425. buttons.push({
  426. id: id_prefix + pos + '_' + id,
  427. svgicon: id,
  428. title: title,
  429. type: 'context',
  430. events: {'click': setArrowFromButton},
  431. panel: 'marker_panel',
  432. list: listname,
  433. isDefault: def
  434. });
  435. def = false;
  436. });
  437. });
  438. return buttons;
  439. }
  440. return {
  441. name: "Markers",
  442. svgicons: svgEditor.curConfig.extPath + "markers-icons.xml",
  443. buttons: buildButtonList(),
  444. context_tools: [
  445. {
  446. type: "input",
  447. panel: "marker_panel",
  448. title: "Start marker",
  449. id: "start_marker",
  450. label: "s",
  451. size: 3,
  452. events: { change: setMarker }
  453. },{
  454. type: "button-select",
  455. panel: "marker_panel",
  456. title: getTitle('en', 'start_marker_list'),
  457. id: "start_marker_list",
  458. colnum: 3,
  459. events: { change: setArrowFromButton }
  460. },{
  461. type: "input",
  462. panel: "marker_panel",
  463. title: "Middle marker",
  464. id: "mid_marker",
  465. label: "m",
  466. defval: "",
  467. size: 3,
  468. events: { change: setMarker }
  469. },{
  470. type: "button-select",
  471. panel: "marker_panel",
  472. title: getTitle('en', 'mid_marker_list'),
  473. id: "mid_marker_list",
  474. colnum: 3,
  475. events: { change: setArrowFromButton }
  476. },{
  477. type: "input",
  478. panel: "marker_panel",
  479. title: "End marker",
  480. id: "end_marker",
  481. label: "e",
  482. size: 3,
  483. events: { change: setMarker }
  484. },{
  485. type: "button-select",
  486. panel: "marker_panel",
  487. title: getTitle('en', 'end_marker_list'),
  488. id: "end_marker_list",
  489. colnum: 3,
  490. events: { change: setArrowFromButton }
  491. } ],
  492. callback: function() {
  493. $('#marker_panel').addClass('toolset').hide();
  494. },
  495. addLangData: function(lang) {
  496. return { data: lang_list[lang] };
  497. },
  498. selectedChanged: function(opts) {
  499. // Use this to update the current selected elements
  500. //console.log('selectChanged',opts);
  501. selElems = opts.elems;
  502. var i = selElems.length;
  503. var marker_elems = ['line','path','polyline','polygon'];
  504. while(i--) {
  505. var elem = selElems[i];
  506. if(elem && $.inArray(elem.tagName, marker_elems) !== -1) {
  507. if (opts.selectedElement && !opts.multiselected) {
  508. showPanel(true);
  509. } else {
  510. showPanel(false);
  511. }
  512. } else {
  513. showPanel(false);
  514. }
  515. }
  516. },
  517. elementChanged: function(opts) {
  518. //console.log('elementChanged',opts);
  519. var elem = opts.elems[0];
  520. if (elem && (
  521. elem.getAttribute("marker-start") ||
  522. elem.getAttribute("marker-mid") ||
  523. elem.getAttribute("marker-end")
  524. )) {
  525. colorChanged(elem);
  526. updateReferences(elem);
  527. }
  528. // changing_flag = false; // Not apparently in use
  529. }
  530. };
  531. });