1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 package net.sourceforge.jeuclid.elements.presentation.token;
20
21 import java.awt.Color;
22 import java.awt.Graphics2D;
23 import java.awt.font.TextLayout;
24 import java.awt.geom.AffineTransform;
25 import java.text.AttributedString;
26
27 import net.sourceforge.jeuclid.Constants;
28 import net.sourceforge.jeuclid.LayoutContext;
29 import net.sourceforge.jeuclid.context.Display;
30 import net.sourceforge.jeuclid.context.Parameter;
31 import net.sourceforge.jeuclid.elements.AbstractJEuclidElement;
32 import net.sourceforge.jeuclid.elements.JEuclidElement;
33 import net.sourceforge.jeuclid.elements.presentation.general.Mrow;
34 import net.sourceforge.jeuclid.elements.support.GraphicsSupport;
35 import net.sourceforge.jeuclid.elements.support.attributes.AttributesHelper;
36 import net.sourceforge.jeuclid.elements.support.operatordict.OperatorDictionary;
37 import net.sourceforge.jeuclid.elements.support.operatordict.OperatorDictionary2;
38 import net.sourceforge.jeuclid.elements.support.operatordict.UnknownAttributeException;
39 import net.sourceforge.jeuclid.elements.support.text.StringUtil;
40 import net.sourceforge.jeuclid.elements.support.text.StringUtil.TextLayoutInfo;
41 import net.sourceforge.jeuclid.layout.LayoutInfo;
42 import net.sourceforge.jeuclid.layout.LayoutStage;
43 import net.sourceforge.jeuclid.layout.LayoutView;
44 import net.sourceforge.jeuclid.layout.TextObject;
45
46 import org.apache.batik.dom.AbstractDocument;
47 import org.apache.batik.dom.events.DOMCustomEvent;
48 import org.w3c.dom.Attr;
49 import org.w3c.dom.Node;
50 import org.w3c.dom.events.CustomEvent;
51 import org.w3c.dom.events.Event;
52 import org.w3c.dom.events.EventListener;
53 import org.w3c.dom.events.EventTarget;
54 import org.w3c.dom.mathml.MathMLOperatorElement;
55 import org.w3c.dom.mathml.MathMLScriptElement;
56 import org.w3c.dom.mathml.MathMLUnderOverElement;
57
58
59
60
61
62
63
64
65
66
67 public final class Mo extends AbstractJEuclidElement implements
68 MathMLOperatorElement, EventListener {
69
70
71
72 public static final String ATTR_FORM = "form";
73
74
75 public static final String ATTR_SEPARATOR = "separator";
76
77
78 public static final String ATTR_LSPACE = "lspace";
79
80
81 public static final String ATTR_RSPACE = "rspace";
82
83
84 public static final String ATTR_MINSIZE = "minsize";
85
86
87 public static final String ATTR_MAXSIZE = "maxsize";
88
89
90 public static final String ATTR_MOVEABLEWRONG = "moveablelimits";
91
92
93 public static final String ATTR_MOVABLELIMITS = "movablelimits";
94
95
96 public static final String ATTR_ACCENT = "accent";
97
98
99
100
101 public static final String ELEMENT = "mo";
102
103
104
105
106 public static final float LARGEOP_CORRECTOR_INLINE = (float) 1.2;
107
108
109
110
111 public static final float LARGEOP_CORRECTOR_BLOCK = (float) 1.5;
112
113
114
115
116 public static final String ATTR_STRETCHY = "stretchy";
117
118
119 public static final String VALUE_STRETCHY_HORIZONTAL = "horizontal";
120
121
122 public static final String VALUE_STRETCHY_VERTICAL = "vertical";
123
124
125
126 public static final String ATTR_LARGEOP = "largeop";
127
128
129
130
131 public static final String ATTR_SYMMETRIC = "symmetric";
132
133
134
135
136 public static final String ATTR_FENCE = "fence";
137
138
139
140
141 public static final String MOEVENT = "MOEvent";
142
143 private static final long serialVersionUID = 1L;
144
145 private final OperatorDictionary opDict;
146
147 private boolean inChangeHook;
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163 public Mo(final String qname, final AbstractDocument odoc) {
164 super(qname, odoc);
165
166 this.setDefaultMathAttribute(Mo.ATTR_FORM,
167 OperatorDictionary.FORM_INFIX);
168 this.setDefaultMathAttribute(Mo.ATTR_FENCE, Constants.FALSE);
169 this.setDefaultMathAttribute(Mo.ATTR_SEPARATOR, Constants.FALSE);
170 this.setDefaultMathAttribute(Mo.ATTR_LSPACE,
171 AttributesHelper.THICKMATHSPACE);
172 this.setDefaultMathAttribute(Mo.ATTR_RSPACE,
173 AttributesHelper.THICKMATHSPACE);
174 this.setDefaultMathAttribute(Mo.ATTR_STRETCHY, Constants.FALSE);
175 this.setDefaultMathAttribute(Mo.ATTR_SYMMETRIC, Constants.TRUE);
176 this
177 .setDefaultMathAttribute(Mo.ATTR_MAXSIZE,
178 AttributesHelper.INFINITY);
179 this.setDefaultMathAttribute(Mo.ATTR_MINSIZE, "1");
180 this.setDefaultMathAttribute(Mo.ATTR_LARGEOP, Constants.FALSE);
181 this.setDefaultMathAttribute(Mo.ATTR_MOVABLELIMITS, Constants.FALSE);
182 this.setDefaultMathAttribute(Mo.ATTR_ACCENT, Constants.FALSE);
183 this.opDict = OperatorDictionary2.getInstance();
184 }
185
186
187 @Override
188 protected Node newNode() {
189 return new Mo(this.nodeName, this.ownerDocument);
190 }
191
192
193
194
195
196
197 private float getLspaceAsFloat(final LayoutContext now) {
198 if (((Integer) now.getParameter(Parameter.SCRIPTLEVEL)) > 0) {
199 return 0.0f;
200 } else {
201 return AttributesHelper.convertSizeToPt(this.getLspace(), now,
202 AttributesHelper.PT);
203 }
204 }
205
206
207
208
209
210
211
212 public float getLargeOpCorrector(final LayoutContext now) {
213 if (Display.BLOCK.equals(now.getParameter(Parameter.DISPLAY))) {
214 return Mo.LARGEOP_CORRECTOR_BLOCK;
215 } else {
216 return Mo.LARGEOP_CORRECTOR_INLINE;
217 }
218 }
219
220
221
222
223
224
225 private float getRspaceAsFloat(final LayoutContext now) {
226 if (((Integer) now.getParameter(Parameter.SCRIPTLEVEL)) > 0) {
227 return 0.0f;
228 } else {
229 return AttributesHelper.convertSizeToPt(this.getRspace(), now,
230 AttributesHelper.PT);
231 }
232 }
233
234 private boolean isFence() {
235 return Boolean.parseBoolean(this.getFence());
236 }
237
238
239
240
241
242
243
244 public void setMaxsize(final String maxsize) {
245 this.setAttribute(Mo.ATTR_MAXSIZE, maxsize);
246 }
247
248
249
250
251
252
253 public String getMaxsize() {
254 return this.getMathAttribute(Mo.ATTR_MAXSIZE);
255 }
256
257
258
259
260
261
262
263 public void setMinsize(final String minsize) {
264 this.setAttribute(Mo.ATTR_MINSIZE, minsize);
265 }
266
267
268
269
270
271
272 public String getMinsize() {
273 return this.getMathAttribute(Mo.ATTR_MINSIZE);
274 }
275
276 private TextLayout produceUnstrechtedLayout(final Graphics2D g,
277 final LayoutContext now) {
278 assert g != null : "Graphics2d is null in produceUnstrechtedLayout";
279 float fontSizeInPoint = GraphicsSupport.getFontsizeInPoint(now);
280 if (Boolean.parseBoolean(this.getLargeop())) {
281 fontSizeInPoint *= this.getLargeOpCorrector(now);
282 }
283
284 final String theText = this.getText();
285 final AttributedString aString = StringUtil
286 .convertStringtoAttributedString(theText, this
287 .getMathvariantAsVariant(), fontSizeInPoint, now);
288 final TextLayout theLayout = StringUtil
289 .createTextLayoutFromAttributedString(g, aString, now);
290 return theLayout;
291 }
292
293
294 @Override
295 public void changeHook() {
296 super.changeHook();
297 if (!this.inChangeHook) {
298 this.inChangeHook = true;
299 this.detectFormParameter();
300 this.loadAttributeFromDictionary(Mo.ATTR_LARGEOP, Constants.FALSE);
301 this.loadAttributeFromDictionary(Mo.ATTR_SYMMETRIC, Constants.TRUE);
302 this.loadAttributeFromDictionary(Mo.ATTR_STRETCHY, Constants.FALSE);
303 this.loadAttributeFromDictionary(Mo.ATTR_FENCE, Constants.FALSE);
304 this.loadAttributeFromDictionary(Mo.ATTR_LSPACE,
305 AttributesHelper.THICKMATHSPACE);
306 this.loadAttributeFromDictionary(Mo.ATTR_RSPACE,
307 AttributesHelper.THICKMATHSPACE);
308 this.loadAttributeFromDictionary(Mo.ATTR_MOVABLELIMITS,
309 Constants.FALSE);
310
311 this.registerWithParentsForEvents();
312 if (this.isFence()) {
313 this.setDefaultMathAttribute(Mo.ATTR_STRETCHY,
314 Mo.VALUE_STRETCHY_VERTICAL);
315 }
316 final CustomEvent evt = new DOMCustomEvent();
317 evt.initCustomEventNS(null, Mo.MOEVENT, true, false, null);
318 this.dispatchEvent(evt);
319 this.inChangeHook = false;
320 }
321 }
322
323 private void registerWithParentsForEvents() {
324 JEuclidElement parent = this.getParent();
325 while (parent != null) {
326 if (parent instanceof EventTarget) {
327 ((EventTarget) parent).addEventListener("DOMSubtreeModified",
328 this, false);
329 }
330 if ((parent instanceof Mrow) && (parent.getMathElementCount() > 1)) {
331 parent = null;
332 } else {
333 parent = parent.getParent();
334 }
335 }
336 }
337
338 private void loadAttributeFromDictionary(final String attrname,
339 final String defvalue) {
340 String attr;
341 try {
342 attr = this.opDict.getDefaultAttributeValue(this.getText(), this
343 .getForm(), attrname);
344 } catch (final UnknownAttributeException e) {
345 attr = defvalue;
346 }
347 if (attr.equals(OperatorDictionary.VALUE_UNKNOWN)) {
348 attr = defvalue;
349 }
350 this.setDefaultMathAttribute(attrname, attr);
351
352 }
353
354 private void detectFormParameter() {
355 final String form;
356 final JEuclidElement parent = this.getParent();
357 if ((parent != null) && (parent instanceof Mrow)) {
358 final int index = parent.getIndexOfMathElement(this);
359 if ((index == 0) && (parent.getMathElementCount() > 0)) {
360 form = OperatorDictionary.FORM_PREFIX;
361 } else {
362 if ((index == (parent.getMathElementCount() - 1))
363 && (parent.getMathElementCount() > 0)) {
364 form = OperatorDictionary.FORM_POSTFIX;
365 } else {
366 form = OperatorDictionary.FORM_INFIX;
367 }
368 }
369 } else {
370 form = OperatorDictionary.FORM_INFIX;
371 }
372 this.setDefaultMathAttribute(Mo.ATTR_FORM, form);
373
374 }
375
376
377 public String getLargeop() {
378 return this.getMathAttribute(Mo.ATTR_LARGEOP);
379 }
380
381
382 public String getLspace() {
383 return this.getMathAttribute(Mo.ATTR_LSPACE);
384 }
385
386
387 public String getMovablelimits() {
388 final String wrongAttr = this.getMathAttribute(Mo.ATTR_MOVEABLEWRONG,
389 false);
390 if (wrongAttr == null) {
391 return this.getMathAttribute(Mo.ATTR_MOVABLELIMITS);
392 } else {
393 return wrongAttr;
394 }
395 }
396
397
398 public String getRspace() {
399 return this.getMathAttribute(Mo.ATTR_RSPACE);
400 }
401
402
403 public void setAccent(final String accent) {
404 this.setAttribute(Mo.ATTR_ACCENT, accent);
405 }
406
407
408 public void setFence(final String fence) {
409 this.setAttribute(Mo.ATTR_FENCE, fence);
410 }
411
412
413 public void setForm(final String form) {
414 this.setAttribute(Mo.ATTR_FORM, form);
415 }
416
417
418 public void setLargeop(final String largeop) {
419 this.setAttribute(Mo.ATTR_LARGEOP, largeop);
420 }
421
422
423 public void setLspace(final String lspace) {
424 this.setAttribute(Mo.ATTR_LSPACE, lspace);
425 }
426
427
428 public void setMovablelimits(final String movablelimits) {
429 this.setAttribute(Mo.ATTR_MOVABLELIMITS, movablelimits);
430 }
431
432
433 public void setRspace(final String rspace) {
434 this.setAttribute(Mo.ATTR_RSPACE, rspace);
435 }
436
437
438 public void setSeparator(final String separator) {
439 this.setAttribute(Mo.ATTR_SEPARATOR, separator);
440 }
441
442
443 public void setStretchy(final String stretchy) {
444 this.setAttribute(Mo.ATTR_STRETCHY, stretchy);
445 }
446
447
448 public void setSymmetric(final String symmetric) {
449 this.setAttribute(Mo.ATTR_SYMMETRIC, symmetric);
450 }
451
452
453 public String getFence() {
454 return this.getMathAttribute(Mo.ATTR_FENCE);
455 }
456
457
458 public String getForm() {
459 return this.getMathAttribute(Mo.ATTR_FORM);
460 }
461
462
463 public String getSeparator() {
464 return this.getMathAttribute(Mo.ATTR_SEPARATOR);
465 }
466
467
468
469
470
471
472
473
474
475 public String getExtendedStretchy() {
476 final String retVal;
477 final Attr attr = this.getAttributeNodeNS(Constants.NS_JEUCLID_EXT,
478 Mo.ATTR_STRETCHY);
479 if (attr == null) {
480 retVal = this.getMathAttribute(Mo.ATTR_STRETCHY);
481 } else {
482 retVal = attr.getValue().trim();
483 }
484 return retVal;
485 }
486
487
488 public String getStretchy() {
489 final String stretchVal = this.getExtendedStretchy();
490 if ((Mo.VALUE_STRETCHY_HORIZONTAL.equalsIgnoreCase(stretchVal))
491 || (Mo.VALUE_STRETCHY_VERTICAL.equalsIgnoreCase(stretchVal))) {
492 return Constants.TRUE;
493 } else {
494 return stretchVal;
495 }
496 }
497
498 private boolean isStretchyHorizontal(final String stretchValue) {
499 return Mo.VALUE_STRETCHY_HORIZONTAL.equalsIgnoreCase(stretchValue)
500 || Boolean.parseBoolean(stretchValue);
501 }
502
503 private boolean isStretchyVertical(final String stretchValue) {
504 return Mo.VALUE_STRETCHY_VERTICAL.equalsIgnoreCase(stretchValue)
505 || Boolean.parseBoolean(stretchValue);
506 }
507
508 private boolean isStretchy() {
509 final String stretchValue = this.getExtendedStretchy();
510 return this.isStretchyHorizontal(stretchValue)
511 || this.isStretchyVertical(stretchValue);
512 }
513
514
515 public String getAccent() {
516 return this.getMathAttribute(Mo.ATTR_ACCENT);
517 }
518
519
520 public String getSymmetric() {
521 return this.getMathAttribute(Mo.ATTR_SYMMETRIC);
522 }
523
524 private boolean isSymmetric() {
525 return Boolean.parseBoolean(this.getSymmetric());
526 }
527
528
529 @Override
530 public void layoutStage1(final LayoutView view, final LayoutInfo info,
531 final LayoutStage childMinStage, final LayoutContext context) {
532 final LayoutContext now = this.applyLocalAttributesToContext(context);
533 final Graphics2D g = view.getGraphics();
534 final TextLayout t = this.produceUnstrechtedLayout(g, now);
535
536 final StringUtil.TextLayoutInfo tli = StringUtil.getTextLayoutInfo(t,
537 true);
538 final float ascent = tli.getAscent();
539 final float descent = tli.getDescent();
540 final float xOffset = tli.getOffset();
541 final float contentWidth = tli.getWidth() + xOffset;
542 final JEuclidElement parent = this.getParent();
543 float lspace = this.getLspaceAsFloat(now);
544 float rspace = this.getRspaceAsFloat(now);
545 if ((parent != null) && (parent.hasChildPostscripts(this, context))) {
546 rspace = 0.0f;
547 } else {
548 rspace = this.getRspaceAsFloat(now);
549 }
550 if ((parent != null) && (parent.hasChildPrescripts(this))) {
551 lspace = 0.0f;
552 } else {
553 lspace = this.getLspaceAsFloat(now);
554 }
555
556 info.setAscentHeight(ascent, LayoutStage.STAGE1);
557 info.setDescentHeight(descent, LayoutStage.STAGE1);
558 info.setHorizontalCenterOffset(lspace + contentWidth / 2.0f,
559 LayoutStage.STAGE1);
560 info.setWidth(lspace + contentWidth + rspace, LayoutStage.STAGE1);
561 if (this.isStretchy()) {
562 info.setLayoutStage(LayoutStage.STAGE1);
563 info.setStretchAscent(0.0f);
564 info.setStretchDescent(0.0f);
565 } else {
566 info.setGraphicsObject(new TextObject(t, lspace + tli.getOffset(),
567 0, null, (Color) now.getParameter(Parameter.MATHCOLOR)));
568 info.setLayoutStage(LayoutStage.STAGE2);
569 }
570 }
571
572
573 @Override
574 public void layoutStage2(final LayoutView view, final LayoutInfo info,
575 final LayoutContext context) {
576 final LayoutContext now = this.applyLocalAttributesToContext(context);
577
578 final Graphics2D g = view.getGraphics();
579 final TextLayout t = this.produceUnstrechtedLayout(g, now);
580
581 final float calcScaleY;
582 final float calcScaleX;
583 final float calcBaselineShift;
584 final String stretchValue = this.getExtendedStretchy();
585 boolean stretchVertically = this.isStretchyVertical(stretchValue);
586 final boolean stretchHorizontally = this
587 .isStretchyHorizontal(stretchValue);
588
589 JEuclidElement horizParent = null;
590 JEuclidElement parent = this;
591 JEuclidElement last;
592 boolean cont;
593 do {
594 last = parent;
595 parent = parent.getParent();
596 cont = false;
597 if ((parent instanceof Mrow) && (parent.getMathElementCount() == 1)) {
598
599 cont = true;
600 } else if ((parent instanceof MathMLUnderOverElement)
601 || (parent instanceof MathMLScriptElement)) {
602
603
604 final boolean isBase;
605 if (parent instanceof MathMLUnderOverElement) {
606 final MathMLUnderOverElement munderover = (MathMLUnderOverElement) parent;
607 isBase = munderover.getBase() == last;
608 } else {
609 final MathMLScriptElement munderover = (MathMLScriptElement) parent;
610 isBase = munderover.getBase() == last;
611 }
612 if (!isBase) {
613 stretchVertically = false;
614 }
615 horizParent = parent;
616 cont = true;
617 }
618 } while (cont);
619 if (horizParent == null) {
620 horizParent = parent;
621 }
622
623 final LayoutInfo parentInfo = view.getInfo(parent);
624 final TextLayoutInfo textLayoutInfo = StringUtil.getTextLayoutInfo(t,
625 true);
626 if (parentInfo == null) {
627 calcScaleX = 1.0f;
628 calcScaleY = 1.0f;
629 calcBaselineShift = 0.0f;
630 } else {
631 if (stretchVertically) {
632 final float[] yf = this.calcYScaleFactorAndBaselineShift(info,
633 parentInfo, textLayoutInfo, now, g);
634 calcScaleY = yf[0];
635 calcBaselineShift = yf[1];
636 } else {
637 calcScaleY = 1.0f;
638 calcBaselineShift = 0.0f;
639 }
640 if (stretchHorizontally) {
641 calcScaleX = this.calcXScaleFactor(info, view
642 .getInfo(horizParent), textLayoutInfo);
643 } else {
644 calcScaleX = 1.0f;
645 }
646 }
647 info.setGraphicsObject(new TextObject(t, this.getLspaceAsFloat(now)
648 + textLayoutInfo.getOffset() * calcScaleX, calcBaselineShift,
649 AffineTransform.getScaleInstance(calcScaleX, calcScaleY),
650 (Color) now.getParameter(Parameter.MATHCOLOR)));
651 info.setLayoutStage(LayoutStage.STAGE2);
652 }
653
654 private float calcXScaleFactor(final LayoutInfo info,
655 final LayoutInfo parentInfo, final TextLayoutInfo textLayoutInfo) {
656 final float calcScaleX;
657 final float rstretchWidth = parentInfo.getStretchWidth();
658 if (rstretchWidth > 0.0f) {
659 final float realwidth = textLayoutInfo.getWidth();
660 if (realwidth > 0) {
661 final float stretchWidth = Math.max(realwidth, rstretchWidth);
662 calcScaleX = stretchWidth / realwidth;
663 info.setWidth(stretchWidth, LayoutStage.STAGE2);
664 info.setHorizontalCenterOffset(stretchWidth / 2.0f,
665 LayoutStage.STAGE2);
666 } else {
667 calcScaleX = 1.0f;
668 }
669 } else {
670 calcScaleX = 1.0f;
671 }
672 return calcScaleX;
673 }
674
675 private float[] calcYScaleFactorAndBaselineShift(final LayoutInfo info,
676 final LayoutInfo parentInfo, final TextLayoutInfo textLayoutInfo,
677 final LayoutContext now, final Graphics2D g2d) {
678 final float calcScaleY;
679 final float calcBaselineShift;
680 final float realDescent = textLayoutInfo.getDescent();
681 final float realAscent = textLayoutInfo.getAscent();
682 float targetNAscent;
683 float targetNDescent;
684 if (this.isFence()) {
685 targetNAscent = Math.max(parentInfo
686 .getAscentHeight(LayoutStage.STAGE1), realAscent);
687 targetNDescent = Math.max(parentInfo
688 .getDescentHeight(LayoutStage.STAGE1), realDescent);
689 } else {
690 targetNAscent = Math.max(parentInfo.getStretchAscent(), realAscent);
691 targetNDescent = Math.max(parentInfo.getStretchDescent(),
692 realDescent);
693 }
694 if (this.isSymmetric()) {
695 final float middle = this.getMiddleShift(g2d, now);
696 final float ascentAboveMiddle = targetNAscent - middle;
697 final float descentBelowMiddle = targetNDescent + middle;
698 final float halfHeight = Math.max(ascentAboveMiddle,
699 descentBelowMiddle);
700 targetNAscent = halfHeight + middle;
701 targetNDescent = halfHeight - middle;
702 }
703 final float targetNHeight = targetNAscent + targetNDescent;
704 final float realHeight = realAscent + realDescent;
705
706
707 final float maxSize = AttributesHelper.parseRelativeSize(this
708 .getMaxsize(), now, realHeight);
709 final float minSize = AttributesHelper.parseRelativeSize(this
710 .getMinsize(), now, realHeight);
711 final float targetHeight = Math.max(Math.min(targetNHeight, maxSize),
712 minSize);
713 final float targetDescent = targetHeight / targetNHeight
714 * (targetNHeight / 2.0f)
715 - (targetNHeight / 2.0f - targetNDescent);
716
717 if (realHeight > 0.0f) {
718 calcScaleY = targetHeight / realHeight;
719 } else {
720 calcScaleY = 1.0f;
721 }
722 final float realDescentScaled = realDescent * calcScaleY;
723 calcBaselineShift = targetDescent - realDescentScaled;
724
725 info.setDescentHeight(targetDescent, LayoutStage.STAGE2);
726 info.setAscentHeight(targetHeight - targetDescent, LayoutStage.STAGE2);
727 return new float[] { calcScaleY, calcBaselineShift };
728 }
729
730
731 public void handleEvent(final Event evt) {
732 this.changeHook();
733 }
734 }