1/**
2 * Copyright (c) 2017, CTI LOGIC
3 * Copyright (c) 2006-2017, JGraph Ltd
4 * Copyright (c) 2006-2017, Gaudenz Alder
5 *
6 * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
7 *
8 * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
9 *
10 * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
11 *
12 * 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
13 *
14 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 'AS IS' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
15 * AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
16 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
17 * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
18 */
19
20function mxRuler(editorUi, unit, isVertical, isSecondery)
21{
22	var requestAnimationFrame = window.requestAnimationFrame || window.mozRequestAnimationFrame ||
23                           		window.webkitRequestAnimationFrame || window.msRequestAnimationFrame;
24	var cancelAnimationFrame = window.cancelAnimationFrame || window.mozCancelAnimationFrame;
25
26	var RULER_THICKNESS = this.RULER_THICKNESS;
27    var ruler = this;
28    this.unit = unit;
29    var style = (!Editor.isDarkMode()) ? {
30    	bkgClr: '#ffffff',
31    	outBkgClr: '#e8e9ed',
32    	cornerClr: '#fbfbfb',
33    	strokeClr: '#dadce0',
34    	fontClr: '#BBBBBB',
35    	guideClr: '#0000BB'
36    } : {
37    	bkgClr: '#202020',
38    	outBkgClr: Editor.darkColor,
39    	cornerClr: Editor.darkColor,
40    	strokeClr: '#505759',
41    	fontClr: '#BBBBBB',
42    	guideClr: '#0088cf'
43    };
44
45    //create the container
46    var container = document.createElement('div');
47    container.style.position = 'absolute';
48
49	function resizeRulerContainer()
50	{
51		var diagCont = editorUi.diagramContainer;
52
53	    container.style.top = (diagCont.offsetTop - RULER_THICKNESS) + 'px';
54	    container.style.left = (diagCont.offsetLeft - RULER_THICKNESS) + 'px';
55	    container.style.width = ((isVertical? 0 : diagCont.offsetWidth) + RULER_THICKNESS) + 'px';
56	    container.style.height = ((isVertical? diagCont.offsetHeight : 0) + RULER_THICKNESS) + 'px';
57	};
58
59	// Hook for dark mode changes
60	this.updateStyle = mxUtils.bind(this, function()
61	{
62		style = (!Editor.isDarkMode()) ? {
63	    	bkgClr: '#ffffff',
64	    	outBkgClr: '#e8e9ed',
65	    	cornerClr: '#fbfbfb',
66	    	strokeClr: '#dadce0',
67	    	fontClr: '#BBBBBB',
68	    	guideClr: '#0000BB'
69	    } : {
70	    	bkgClr: '#202020',
71	    	outBkgClr: Editor.darkColor,
72	    	cornerClr: Editor.darkColor,
73	    	strokeClr: '#505759',
74	    	fontClr: '#BBBBBB',
75	    	guideClr: '#0088cf'
76	    };
77
78	    container.style.background = style.bkgClr;
79	    container.style[isVertical? 'borderRight' : 'borderBottom'] = '0.5px solid ' + style.strokeClr;
80		container.style.borderLeft = '0.5px solid ' + style.strokeClr;
81	});
82
83	this.updateStyle();
84
85    document.body.appendChild(container);
86	mxEvent.disableContextMenu(container);
87
88	this.editorUiRefresh = editorUi.refresh;
89
90	editorUi.refresh = function(minor)
91	{
92		ruler.editorUiRefresh.apply(editorUi, arguments);
93
94		resizeRulerContainer();
95	};
96
97	resizeRulerContainer();
98
99    var canvas = document.createElement('canvas');
100    //initial sizing which is corrected by the graph size event
101    canvas.width = container.offsetWidth;
102    canvas.height = container.offsetHeight;
103    container.style.overflow = 'hidden';
104    canvas.style.position = 'relative';
105    container.appendChild(canvas);
106    //Disable alpha to improve performance as we don't need it?
107    var ctx = canvas.getContext('2d');
108    this.ui = editorUi;
109    var graph = editorUi.editor.graph;
110    this.graph = graph;
111    this.container = container;
112    this.canvas = canvas;
113
114    var drawLine = function (x1, y1, x2, y2, text)
115    {
116        //remove all fractions
117        x1 = Math.round(x1); y1 = Math.round(y1); x2 = Math.round(x2); y2 = Math.round(y2);
118        //adding the 0.5 is necessary to prevent anti-aliasing from making lines thicker!
119        ctx.beginPath();
120        ctx.moveTo(x1 + 0.5, y1 + 0.5);
121        ctx.lineTo(x2 + 0.5, y2 + 0.5);
122        ctx.stroke();
123
124        if (text)
125        {
126            if (isVertical)
127            {
128                ctx.save();
129                ctx.translate(x1, y1);
130                ctx.rotate(-Math.PI / 2);
131                ctx.fillText(text, 0, 0);
132                ctx.restore();
133            }
134            else
135            {
136                ctx.fillText(text, x1, y1);
137            }
138        }
139    };
140
141    var drawRuler = function()
142    {
143    	ctx.clearRect(0, 0, canvas.width, canvas.height);
144
145        ctx.beginPath();
146        ctx.lineWidth = 0.7;
147        ctx.strokeStyle = style.strokeClr;
148        ctx.setLineDash([]);
149        ctx.font = '9px Arial';
150        ctx.textAlign = 'center';
151
152        var scale = graph.view.scale;
153        var bgPages = graph.view.getBackgroundPageBounds();
154        var t = graph.view.translate;
155        var hasPageView = graph.pageVisible;
156
157        //The beginning of the ruler (zero)
158        var rStart = hasPageView? RULER_THICKNESS + (isVertical? bgPages.y -  graph.container.scrollTop : bgPages.x - graph.container.scrollLeft)
159        		: RULER_THICKNESS + (isVertical? t.y * scale -  graph.container.scrollTop : t.x * scale - graph.container.scrollLeft);
160
161        //handle negative pages
162        var pageShift = 0;
163
164        if (hasPageView)
165        {
166			var layout = graph.getPageLayout();
167
168	        if (isVertical)
169	        {
170	            pageShift = layout.y * graph.pageFormat.height;
171	        }
172	        else
173	        {
174	            pageShift = layout.x * graph.pageFormat.width;
175	        }
176        }
177
178        var tickStep, tickSize, len;
179
180        switch(ruler.unit)
181        {
182            case mxConstants.POINTS:
183                len = 10;
184                tickStep = 10;
185                tickSize = [3,5,5,5,5,10,5,5,5,5];
186                break;
187            case mxConstants.MILLIMETERS:
188                len = 10;
189                tickStep = mxConstants.PIXELS_PER_MM;
190                tickSize = [5,3,3,3,3,6,3,3,3,3];
191                break;
192			case mxConstants.METERS:
193                len = 20;
194                tickStep = mxConstants.PIXELS_PER_MM;
195                tickSize = [5,3,3,3,3,6,3,3,3,3,10,3,3,3,3,6,3,3,3,3];
196                break;
197            case mxConstants.INCHES:
198            	if (scale <=0.5 || scale >=4)
199                    len = 8;
200                else
201                    len = 16;
202
203                tickStep = mxConstants.PIXELS_PER_INCH / len;
204                tickSize = [5,3,5,3,7,3,5,3,7,3,5,3,7,3,5,3];
205                break;
206        }
207
208        //Handle step size and change it with large/small scale
209        var step = tickStep;
210
211        if (scale >= 2)
212    	{
213        	step = tickStep / (Math.floor(scale / 2) * 2);
214    	}
215        else if (scale <= 0.5)
216    	{
217        	step = tickStep * (Math.floor((1 / scale) / 2) * (ruler.unit == mxConstants.MILLIMETERS? 2 : 1));
218    	}
219
220        var lastTick = null;
221
222        //End of the ruler (pages end)
223        var rEnd = hasPageView? Math.min(rStart + (isVertical? bgPages.height: bgPages.width), isVertical? canvas.height : canvas.width) : (isVertical? canvas.height : canvas.width);
224
225        if (hasPageView)
226        {
227	        //Clear the outside page part with a different color
228	        ctx.fillStyle = style.outBkgClr;
229
230	        if (isVertical)
231	    	{
232				var oh = rStart - RULER_THICKNESS;
233
234				if (oh > 0)
235				{
236					ctx.fillRect(0, RULER_THICKNESS, RULER_THICKNESS, oh);
237				}
238
239				if (rEnd < canvas.height)
240				{
241					ctx.fillRect(0, rEnd, RULER_THICKNESS, canvas.height);
242				}
243	    	}
244	        else
245	        {
246				var ow = rStart - RULER_THICKNESS;
247
248				if (ow > 0)
249				{
250					ctx.fillRect(RULER_THICKNESS, 0, ow, RULER_THICKNESS);
251				}
252
253				if (rEnd < canvas.width)
254				{
255					ctx.fillRect(rEnd, 0, canvas.width, RULER_THICKNESS);
256				}
257	        }
258        }
259
260        //Draw ticks
261        ctx.fillStyle = style.fontClr;
262
263        for (var i = hasPageView? rStart : rStart % (step * scale); i <= rEnd; i += step * scale)
264        {
265        	 var current = Math.round((i - rStart) / scale / step);
266
267        	 if (i < RULER_THICKNESS || current == lastTick) //Prevent wasting time in drawing non-visible/duplicate lines
268         	 {
269             	 continue;
270         	 }
271
272             lastTick = current;
273             var text = null;
274
275             if (current % len == 0)
276             {
277                 text = ruler.formatText(pageShift + current * step) + '';
278             }
279
280        	 if (isVertical)
281             {
282                 drawLine(RULER_THICKNESS - tickSize[Math.abs(current) % len], i, RULER_THICKNESS, i, text);
283             }
284             else
285             {
286                 drawLine(i, RULER_THICKNESS - tickSize[Math.abs(current) % len], i, RULER_THICKNESS, text);
287             }
288        }
289
290        //Draw corner rect
291        ctx.lineWidth = 1;
292        drawLine(isVertical? 0 : RULER_THICKNESS, isVertical? RULER_THICKNESS : 0, RULER_THICKNESS, RULER_THICKNESS);
293        ctx.fillStyle = style.cornerClr;
294        ctx.fillRect(0, 0, RULER_THICKNESS, RULER_THICKNESS);
295    };
296
297	var animationId = -1;
298
299	var listenersDrawRuler = function()
300	{
301		if (requestAnimationFrame != null)
302		{
303			if (cancelAnimationFrame != null)
304			{
305				cancelAnimationFrame(animationId);
306			}
307
308			animationId = requestAnimationFrame(drawRuler);
309		}
310		else
311		{
312			drawRuler();
313		}
314	};
315
316	var sizeListener = function()
317	{
318	    var div = graph.container;
319
320	    if (isVertical)
321    	{
322	    	var newH = div.offsetHeight + RULER_THICKNESS;
323
324	    	if (canvas.height != newH)
325    		{
326	    		canvas.height = newH;
327	    		container.style.height = newH + 'px';
328	    		listenersDrawRuler();
329    		}
330    	}
331	    else
332    	{
333	    	var newW = div.offsetWidth + RULER_THICKNESS;
334
335	    	if (canvas.width != newW)
336    		{
337	    		canvas.width = newW;
338	    		container.style.width = newW + 'px';
339	    		listenersDrawRuler();
340    		}
341    	}
342    };
343
344    this.drawRuler = listenersDrawRuler;
345
346	var efficientSizeListener = debounce(sizeListener, 10);
347	this.sizeListener = efficientSizeListener;
348
349	this.pageListener = function()
350	{
351		listenersDrawRuler();
352	};
353
354	var efficientScrollListener = debounce(function()
355	{
356		var newScroll = isVertical? graph.container.scrollTop : graph.container.scrollLeft;
357
358		if (ruler.lastScroll != newScroll)
359		{
360			ruler.lastScroll = newScroll;
361			listenersDrawRuler();
362		}
363	}, 10);
364
365	this.scrollListener = efficientScrollListener;
366	this.unitListener = function(sender, evt)
367    {
368    	ruler.setUnit(evt.getProperty('unit'));
369    };
370
371    graph.addListener(mxEvent.SIZE, efficientSizeListener);
372    graph.container.addEventListener('scroll', efficientScrollListener);
373    graph.view.addListener('unitChanged', this.unitListener);
374    editorUi.addListener('pageViewChanged', this.pageListener);
375    editorUi.addListener('pageScaleChanged', this.pageListener);
376    editorUi.addListener('pageFormatChanged', this.pageListener);
377
378    function debounce(func, wait, immediate)
379    {
380		if (requestAnimationFrame != null)
381		{
382			return func;
383		}
384
385        var timeout;
386        return function() {
387            var context = this, args = arguments;
388            var later = function() {
389                timeout = null;
390                if (!immediate) func.apply(context, args);
391            };
392            var callNow = immediate && !timeout;
393            clearTimeout(timeout);
394            timeout = setTimeout(later, wait);
395            if (callNow) func.apply(context, args);
396        };
397    };
398
399    this.setStyle = function(newStyle)
400    {
401    	style = newStyle;
402    	container.style.background = style.bkgClr;
403    	drawRuler();
404    }
405
406    // Showing guides on cell move
407    this.origGuideMove = mxGuide.prototype.move;
408
409	mxGuide.prototype.move = function (bounds, delta, gridEnabled, clone)
410	{
411		var ret = null;
412
413		// LATER: Fix repaint for width and height < 5
414		if ((isVertical && bounds.height > 4) || (!isVertical && bounds.width > 4))
415		{
416			if (ruler.guidePart != null)
417			{
418				try
419				{
420					ctx.putImageData(ruler.guidePart.imgData1, ruler.guidePart.x1, ruler.guidePart.y1);
421					ctx.putImageData(ruler.guidePart.imgData2, ruler.guidePart.x2, ruler.guidePart.y2);
422					ctx.putImageData(ruler.guidePart.imgData3, ruler.guidePart.x3, ruler.guidePart.y3);
423				}
424				catch (e)
425				{
426					// ignore
427				}
428			}
429
430			ret = ruler.origGuideMove.apply(this, arguments);
431
432			try
433			{
434				var x1, y1, imgData1, x2, y2, imgData2, x3, y3, imgData3;
435				ctx.lineWidth = 0.5;
436		        ctx.strokeStyle = style.guideClr;
437		        ctx.setLineDash([2]);
438
439		        if (isVertical)
440				{
441					y1 = bounds.y + ret.y + RULER_THICKNESS - this.graph.container.scrollTop;
442					x1 = 0;
443					y2 = y1 + bounds.height / 2;
444					x2 = RULER_THICKNESS / 2;
445					y3 = y1 + bounds.height;
446					x3 = 0;
447					imgData1 = ctx.getImageData(x1, y1 - 1, RULER_THICKNESS, 3);
448					drawLine(x1, y1, RULER_THICKNESS, y1);
449					y1--;
450					imgData2 = ctx.getImageData(x2, y2 - 1, RULER_THICKNESS, 3);
451					drawLine(x2, y2, RULER_THICKNESS, y2);
452					y2--;
453					imgData3 = ctx.getImageData(x3, y3 - 1, RULER_THICKNESS, 3);
454					drawLine(x3, y3, RULER_THICKNESS, y3);
455					y3--;
456				}
457				else
458				{
459					y1 = 0;
460					x1 = bounds.x + ret.x + RULER_THICKNESS - this.graph.container.scrollLeft;
461					y2 = RULER_THICKNESS / 2;
462					x2 = x1 + bounds.width / 2;
463					y3 = 0;
464					x3 = x1 + bounds.width;
465					imgData1 = ctx.getImageData(x1 - 1, y1, 3, RULER_THICKNESS);
466					drawLine(x1, y1, x1, RULER_THICKNESS);
467					x1--;
468					imgData2 = ctx.getImageData(x2 - 1, y2, 3, RULER_THICKNESS);
469					drawLine(x2, y2, x2, RULER_THICKNESS);
470					x2--;
471					imgData3 = ctx.getImageData(x3 - 1, y3, 3, RULER_THICKNESS);
472					drawLine(x3, y3, x3, RULER_THICKNESS);
473					x3--;
474				}
475
476				if (ruler.guidePart == null || ruler.guidePart.x1 != x1 || ruler.guidePart.y1 != y1)
477				{
478					ruler.guidePart = {
479						imgData1: imgData1,
480						x1: x1,
481						y1: y1,
482						imgData2: imgData2,
483						x2: x2,
484						y2: y2,
485						imgData3: imgData3,
486						x3: x3,
487						y3: y3
488					}
489				}
490			}
491			catch (e)
492			{
493				// ignore
494			}
495		}
496		else
497		{
498			ret = ruler.origGuideMove.apply(this, arguments);
499		}
500
501		return ret;
502	}
503
504	this.origGuideDestroy = mxGuide.prototype.destroy;
505
506	mxGuide.prototype.destroy = function()
507	{
508		var ret = ruler.origGuideDestroy.apply(this, arguments);
509
510		if (ruler.guidePart != null)
511		{
512			try
513			{
514				ctx.putImageData(ruler.guidePart.imgData1, ruler.guidePart.x1, ruler.guidePart.y1);
515				ctx.putImageData(ruler.guidePart.imgData2, ruler.guidePart.x2, ruler.guidePart.y2);
516				ctx.putImageData(ruler.guidePart.imgData3, ruler.guidePart.x3, ruler.guidePart.y3);
517				ruler.guidePart = null;
518			}
519			catch (e)
520			{
521				// ignore
522			}
523		}
524
525		return ret;
526	};
527};
528
529mxRuler.prototype.RULER_THICKNESS = 14;
530mxRuler.prototype.unit = mxConstants.POINTS;
531
532mxRuler.prototype.setUnit = function(unit)
533{
534    this.unit = unit;
535    this.drawRuler();
536};
537
538mxRuler.prototype.formatText = function(pixels)
539{
540    switch(this.unit)
541    {
542        case mxConstants.POINTS:
543            return Math.round(pixels);
544        case mxConstants.MILLIMETERS:
545            return (pixels / mxConstants.PIXELS_PER_MM).toFixed(1);
546        case mxConstants.METERS:
547            return (pixels / (mxConstants.PIXELS_PER_MM * 1000)).toFixed(4);
548        case mxConstants.INCHES:
549            return (pixels / mxConstants.PIXELS_PER_INCH).toFixed(2);
550    }
551};
552
553mxRuler.prototype.destroy = function()
554{
555	this.ui.refresh = this.editorUiRefresh;
556	mxGuide.prototype.move = this.origGuideMove;
557	mxGuide.prototype.destroy = this.origGuideDestroy;
558    this.graph.removeListener(this.sizeListener);
559    this.graph.container.removeEventListener('scroll', this.scrollListener);
560    this.graph.view.removeListener('unitChanged', this.unitListener);
561    this.ui.removeListener('pageViewChanged', this.pageListener);
562    this.ui.removeListener('pageScaleChanged', this.pageListener);
563    this.ui.removeListener('pageFormatChanged', this.pageListener);
564
565    if (this.container != null)
566    {
567    	this.container.parentNode.removeChild(this.container);
568    }
569};
570
571function mxDualRuler(editorUi, unit)
572{
573	var rulerOffset = new mxPoint(mxRuler.prototype.RULER_THICKNESS, mxRuler.prototype.RULER_THICKNESS);
574	this.editorUiGetDiagContOffset = editorUi.getDiagramContainerOffset;
575
576	editorUi.getDiagramContainerOffset = function()
577	{
578		return rulerOffset;
579	};
580
581	this.editorUiRefresh = editorUi.refresh;
582	this.ui = editorUi;
583	this.origGuideMove = mxGuide.prototype.move;
584	this.origGuideDestroy = mxGuide.prototype.destroy;
585
586	this.vRuler = new mxRuler(editorUi, unit, true);
587	this.hRuler = new mxRuler(editorUi, unit, false, true);
588
589	// Adds units context menu
590	var installMenu = mxUtils.bind(this, function(node)
591	{
592		var menuWasVisible = false;
593
594		mxEvent.addGestureListeners(node, mxUtils.bind(this, function(evt)
595		{
596			menuWasVisible = editorUi.currentMenu != null;
597			mxEvent.consume(evt);
598		}), null, mxUtils.bind(this, function(evt)
599		{
600			if (editorUi.editor.graph.isEnabled() && !editorUi.editor.graph.isMouseDown &&
601				(mxEvent.isTouchEvent(evt) || mxEvent.isPopupTrigger(evt)))
602			{
603				editorUi.editor.graph.popupMenuHandler.hideMenu();
604				editorUi.hideCurrentMenu();
605
606				if (!mxEvent.isTouchEvent(evt) || !menuWasVisible)
607				{
608					var menu = new mxPopupMenu(mxUtils.bind(this, function(menu, parent)
609					{
610						editorUi.menus.addMenuItems(menu, ['points', 'inches', 'millimeters', 'meters'], parent);
611					}));
612
613					menu.div.className += ' geMenubarMenu';
614					menu.smartSeparators = true;
615					menu.showDisabled = true;
616					menu.autoExpand = true;
617
618					// Disables autoexpand and destroys menu when hidden
619					menu.hideMenu = mxUtils.bind(this, function()
620					{
621						mxPopupMenu.prototype.hideMenu.apply(menu, arguments);
622						editorUi.resetCurrentMenu();
623						menu.destroy();
624					});
625
626					var x = mxEvent.getClientX(evt);
627					var y = mxEvent.getClientY(evt);
628					menu.popup(x, y, null, evt);
629					editorUi.setCurrentMenu(menu, node);
630				}
631
632				mxEvent.consume(evt);
633			}
634		}));
635	});
636
637	installMenu(this.hRuler.container);
638	installMenu(this.vRuler.container);
639
640	this.vRuler.drawRuler();
641	this.hRuler.drawRuler();
642};
643
644mxDualRuler.prototype.updateStyle = function()
645{
646	this.vRuler.updateStyle();
647	this.hRuler.updateStyle();
648	this.vRuler.drawRuler();
649	this.hRuler.drawRuler();
650};
651
652mxDualRuler.prototype.setUnit = function(unit)
653{
654	this.vRuler.setUnit(unit);
655	this.hRuler.setUnit(unit);
656};
657
658mxDualRuler.prototype.setStyle = function(newStyle)
659{
660	this.vRuler.setStyle(newStyle);
661	this.hRuler.setStyle(newStyle);
662}
663
664mxDualRuler.prototype.destroy = function()
665{
666	this.vRuler.destroy();
667	this.hRuler.destroy();
668	this.ui.refresh = this.editorUiRefresh;
669	mxGuide.prototype.move = this.origGuideMove;
670	mxGuide.prototype.destroy = this.origGuideDestroy;
671	this.ui.getDiagramContainerOffset = this.editorUiGetDiagContOffset;
672};