ext-markers.js 16 KB

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