001/*
002 * This library is part of OpenCms -
003 * the Open Source Content Management System
004 *
005 * Copyright (c) Alkacon Software GmbH & Co. KG (http://www.alkacon.com)
006 *
007 * This library is free software; you can redistribute it and/or
008 * modify it under the terms of the GNU Lesser General Public
009 * License as published by the Free Software Foundation; either
010 * version 2.1 of the License, or (at your option) any later version.
011 *
012 * This library is distributed in the hope that it will be useful,
013 * but WITHOUT ANY WARRANTY; without even the implied warranty of
014 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
015 * Lesser General Public License for more details.
016 *
017 * For further information about Alkacon Software, please see the
018 * company website: http://www.alkacon.com
019 *
020 * For further information about OpenCms, please see the
021 * project website: http://www.opencms.org
022 *
023 * You should have received a copy of the GNU Lesser General Public
024 * License along with this library; if not, write to the Free Software
025 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
026 */
027
028package org.opencms.gwt.client.util;
029
030import org.opencms.gwt.client.util.CmsDomUtil.Style;
031
032import com.google.gwt.dom.client.Element;
033import com.google.gwt.dom.client.Style.Display;
034import com.google.gwt.dom.client.Style.Position;
035import com.google.gwt.user.client.Window;
036import com.google.gwt.user.client.ui.UIObject;
037
038/**
039 * Bean holding the position data of a HTML DOM element.<p>
040 *
041 * @since 8.0.0
042 */
043public class CmsPositionBean {
044
045    /** Position area. */
046    public static enum Area {
047
048        /** Bottom border. */
049        BORDER_BOTTOM,
050
051        /** Left border. */
052        BORDER_LEFT,
053
054        /** Right border. */
055        BORDER_RIGHT,
056
057        /** Top border. */
058        BORDER_TOP,
059
060        /** The center. */
061        CENTER,
062
063        /** Bottom left corner. */
064        CORNER_BOTTOM_LEFT,
065
066        /** Bottom right corner. */
067        CORNER_BOTTOM_RIGHT,
068
069        /** Top left corner. */
070        CORNER_TOP_LEFT,
071
072        /** Top right corner. */
073        CORNER_TOP_RIGHT
074    }
075
076    /** The directions. */
077    static enum Direction {
078        /** Bottom. */
079        bottom,
080
081        /** Left. */
082        left,
083
084        /** Right. */
085
086        right,
087
088        /** Top. */
089        top
090    }
091
092    /** Element height. */
093    private int m_height;
094
095    /** Position left. */
096    private int m_left;
097
098    /** Position top. */
099    private int m_top;
100
101    /** Element width. */
102    private int m_width;
103
104    /**
105     * Constructor.<p>
106     */
107    public CmsPositionBean() {
108
109        // default constructor
110    }
111
112    /**
113     * Copy constructor. Generating a copy of the given model.<p>
114     *
115     * @param model the model to copy
116     */
117    public CmsPositionBean(CmsPositionBean model) {
118
119        m_height = model.getHeight();
120        m_left = model.getLeft();
121        m_top = model.getTop();
122        m_width = model.getWidth();
123    }
124
125    /**
126     * Manipulates the position infos to ensure a minimum margin between the rectangles.<p>
127     *
128     * @param posA the first position to check
129     * @param posB the second position to check
130     * @param margin the required margin
131     */
132    public static void avoidCollision(CmsPositionBean posA, CmsPositionBean posB, int margin) {
133
134        Direction dir = null;
135        int diff = 0;
136        int diffTemp = (posB.getLeft() + posB.getWidth()) - posA.getLeft();
137        if (diffTemp > -margin) {
138            dir = Direction.left;
139            diff = diffTemp;
140        }
141
142        diffTemp = (posA.getLeft() + posA.getWidth()) - posB.getLeft();
143        if ((diffTemp > -margin) && (diffTemp < diff)) {
144            dir = Direction.right;
145            diff = diffTemp;
146        }
147        diffTemp = (posB.getTop() + posB.getHeight()) - posA.getTop();
148        if ((diffTemp > -margin) && (diffTemp < diff)) {
149            dir = Direction.top;
150            diff = diffTemp;
151        }
152        diffTemp = (posA.getTop() + posA.getHeight()) - posB.getTop();
153        if ((diffTemp > -margin) && (diffTemp < diff)) {
154            dir = Direction.bottom;
155            diff = diffTemp;
156        }
157
158        diff = (int)Math.ceil((1.0 * (diff + margin)) / 2);
159        if (dir != null) {
160            switch (dir) {
161                case left:
162                    // move the left border of a
163                    posA.setLeft(posA.getLeft() + diff);
164                    posA.setWidth(posA.getWidth() - diff);
165
166                    // move the right border of b
167                    posB.setWidth(posB.getWidth() - diff);
168                    break;
169
170                case right:
171                    // move the left border of b
172                    posB.setLeft(posB.getLeft() + diff);
173                    posB.setWidth(posB.getWidth() - diff);
174
175                    // move the right border of a
176                    posA.setWidth(posA.getWidth() - diff);
177                    break;
178
179                case top:
180                    posA.setTop(posA.getTop() + diff);
181                    posA.setHeight(posA.getHeight() - diff);
182
183                    posB.setHeight(posB.getHeight() - diff);
184                    break;
185                case bottom:
186                    posB.setTop(posB.getTop() + diff);
187                    posB.setHeight(posB.getHeight() - diff);
188
189                    posA.setHeight(posA.getHeight() - diff);
190                    break;
191                default:
192                    // nothing to do
193            }
194        }
195    }
196
197    /**
198     * Checks whether the two position rectangles collide.<p>
199     *
200     * @param posA the first position to check
201     * @param posB the second position to check
202     * @param margin the required margin
203     *
204     * @return <code>true</code> if the two position rectangles collide
205     */
206    public static boolean checkCollision(CmsPositionBean posA, CmsPositionBean posB, int margin) {
207
208        // check for non collision is easier
209        if ((posA.getLeft() - margin) >= (posB.getLeft() + posB.getWidth())) {
210            // posA is right of posB
211            return false;
212        }
213        if ((posA.getLeft() + posA.getWidth()) <= (posB.getLeft() - margin)) {
214            // posA is left of posB
215            return false;
216        }
217        if ((posA.getTop() - margin) >= (posB.getTop() + posB.getHeight())) {
218            // posA is bellow posB
219            return false;
220        }
221        if ((posA.getTop() + posA.getHeight()) <= (posB.getTop() - margin)) {
222            // posA is above posB
223            return false;
224        }
225
226        // in any other case the position rectangles collide
227        return true;
228    }
229
230    /**
231     * Collects the position information of the given UI object and returns a position info bean.<p>
232     *
233     * @param element the object to read the position data from
234     *
235     * @return the position data
236     */
237    public static CmsPositionBean generatePositionInfo(Element element) {
238
239        CmsPositionBean result = new CmsPositionBean();
240        result.setHeight(element.getOffsetHeight());
241        result.setWidth(element.getOffsetWidth());
242        result.setTop(element.getAbsoluteTop());
243        result.setLeft(element.getAbsoluteLeft());
244        return result;
245    }
246
247    /**
248     * Collects the position information of the given UI object and returns a position info bean.<p>
249     *
250     * @param uiObject the object to read the position data from
251     *
252     * @return the position data
253     */
254    public static CmsPositionBean generatePositionInfo(UIObject uiObject) {
255
256        return generatePositionInfo(uiObject.getElement());
257    }
258
259    /**
260     * Returns the bounding rectangle dimensions of the element including all floated elements.<p>
261     *
262     * @param panel the panel
263     *
264     * @return the position info
265     */
266    public static CmsPositionBean getBoundingClientRect(Element panel) {
267
268        return getBoundingClientRect(panel, true);
269
270    }
271
272    /**
273     * Returns the bounding rectangle dimensions of the element including all floated elements.<p>
274     *
275     * @param panel the panel
276     * @param addScroll if true, the result will contain the coordinates in the document's coordinate system, not the viewport coordinate system
277     *
278     * @return the position info
279     */
280    public static CmsPositionBean getBoundingClientRect(Element panel, boolean addScroll) {
281
282        CmsPositionBean result = new CmsPositionBean();
283        getBoundingClientRect(
284            panel,
285            result,
286            addScroll ? Window.getScrollLeft() : 0,
287            addScroll ? Window.getScrollTop() : 0);
288        return result;
289    }
290
291    /**
292     * Returns a position info representing the dimensions of all visible child elements of the given panel (excluding elements with position:absolute).
293     * If the panel has no visible child elements, it's outer dimensions are returned.<p>
294     *
295     * @param panel the panel
296     *
297     * @return the position info
298     */
299    public static CmsPositionBean getInnerDimensions(Element panel) {
300
301        boolean first = true;
302        int top = 0;
303        int left = 0;
304        int bottom = 0;
305        int right = 0;
306        Element child = panel.getFirstChildElement();
307        while (child != null) {
308            String tagName = child.getTagName();
309            if (tagName.equalsIgnoreCase("br")
310                || tagName.equalsIgnoreCase("tr")
311                || tagName.equalsIgnoreCase("thead")
312                || tagName.equalsIgnoreCase("tfoot")
313                || tagName.equalsIgnoreCase("script")
314                || tagName.equalsIgnoreCase("style")) {
315                // ignore tags with no relevant position info
316                child = child.getNextSiblingElement();
317                continue;
318            }
319            String positioning = CmsDomUtil.getCurrentStyle(child, Style.position);
320            if (!Display.NONE.getCssName().equals(CmsDomUtil.getCurrentStyle(child, Style.display))
321                && !(positioning.equalsIgnoreCase(Position.ABSOLUTE.getCssName())
322                    || positioning.equalsIgnoreCase(Position.FIXED.getCssName()))) {
323                CmsPositionBean childDimensions = getBoundingClientRect(child);
324                if (first) {
325                    first = false;
326                    top = childDimensions.getTop();
327                    left = childDimensions.getLeft();
328                    bottom = top + childDimensions.getHeight();
329                    right = left + childDimensions.getWidth();
330                } else {
331                    int wTop = childDimensions.getTop();
332                    top = top < wTop ? top : wTop;
333                    int wLeft = childDimensions.getLeft();
334                    left = left < wLeft ? left : wLeft;
335                    int wBottom = wTop + childDimensions.getHeight();
336                    bottom = bottom > wBottom ? bottom : wBottom;
337                    int wRight = wLeft + childDimensions.getWidth();
338                    right = right > wRight ? right : wRight;
339                }
340            }
341            child = child.getNextSiblingElement();
342        }
343        if (!first) {
344            CmsPositionBean result = new CmsPositionBean();
345            result.setHeight(bottom - top);
346            result.setWidth(right - left);
347            result.setTop(top);
348            result.setLeft(left);
349            return result;
350        } else {
351            return getBoundingClientRect(panel);
352        }
353    }
354
355    /**
356     * Checks whether a value is in a given interval (including the end points).<p>
357     *
358     * @param min the minimum of the interval
359     * @param max the maximum of the interval
360     * @param value the value to check
361     *
362     * @return true if the value is in the given interval
363     */
364    public static boolean isInRangeInclusive(int min, int max, int value) {
365
366        return (min <= value) && (value <= max);
367    }
368
369    /**
370     * Uses the getBoundingClientRect method to evaluate the element dimensions.<p>
371     *
372     * @param element the element
373     * @param pos the position bean
374     * @param scrollLeft the window scroll position left
375     * @param scrollTop the window scroll position top
376     */
377    private static native void getBoundingClientRect(
378        Element element,
379        CmsPositionBean pos,
380        int scrollLeft,
381        int scrollTop)/*-{
382
383                      var rect = element.getBoundingClientRect();
384                      pos.@org.opencms.gwt.client.util.CmsPositionBean::m_top=Math.round(rect.top+scrollTop);
385                      pos.@org.opencms.gwt.client.util.CmsPositionBean::m_left=Math.round(rect.left+scrollLeft);
386                      pos.@org.opencms.gwt.client.util.CmsPositionBean::m_height=Math.round(rect.height);
387                      pos.@org.opencms.gwt.client.util.CmsPositionBean::m_width=Math.round(rect.width);
388                      }-*/;
389
390    /**
391     * Checks if the rectangle defined by this bean contains the given point.<p>
392     *
393     * @param x the horizontal coordinate
394     * @param y the vertical coordinate
395     *
396     * @return true if this object contains the given point
397     */
398    public boolean containsPoint(int x, int y) {
399
400        return isInRangeInclusive(getLeft(), (getLeft() + getWidth()) - 1, x)
401            && isInRangeInclusive(getTop(), (getTop() + getHeight()) - 1, y);
402    }
403
404    /**
405     * Increases the dimensions to completely surround the child.<p>
406     *
407     * @param child the child position info
408     * @param padding the padding to apply
409     */
410    public void ensureSurrounds(CmsPositionBean child, int padding) {
411
412        // increase the size of the outer rectangle
413        if ((getLeft() + padding) > child.getLeft()) {
414            int diff = getLeft() - child.getLeft();
415            // ensure padding
416            diff += padding;
417            setLeft(getLeft() - diff);
418            setWidth(getWidth() + diff);
419        }
420        if ((getTop() + padding) > child.getTop()) {
421            int diff = getTop() - child.getTop();
422            diff += padding;
423            setTop(getTop() - diff);
424            setHeight(getHeight() + diff);
425        }
426        if ((getLeft() + getWidth()) < (child.getLeft() + child.getWidth() + padding)) {
427            int diff = (child.getLeft() + child.getWidth()) - (getLeft() + getWidth());
428            diff += padding;
429            setWidth(getWidth() + diff);
430        }
431        if ((getTop() + getHeight()) < (child.getTop() + child.getHeight() + padding)) {
432            int diff = (child.getTop() + child.getHeight()) - (getTop() + getHeight());
433            diff += padding;
434            setHeight(getHeight() + diff);
435        }
436    }
437
438    /**
439     * Returns over which area of this the given position is. Will return <code>null</code> if the provided position is not within this position.<p>
440     *
441     * @param absLeft the left position
442     * @param absTop the right position
443     * @param offset the border offset
444     *
445     * @return the area
446     */
447    public Area getArea(int absLeft, int absTop, int offset) {
448
449        if (isOverElement(absLeft, absTop)) {
450            if (absLeft < (m_left + 10)) {
451                // left border
452                if (absTop < (m_top + offset)) {
453                    // top left corner
454                    return Area.CORNER_TOP_LEFT;
455                } else if (absTop > ((m_top + m_height) - offset)) {
456                    // bottom left corner
457                    return Area.CORNER_BOTTOM_LEFT;
458                }
459                return Area.BORDER_LEFT;
460            }
461            if (absLeft > ((m_left + m_width) - offset)) {
462                // right border
463                if (absTop < (m_top + offset)) {
464                    // top right corner
465                    return Area.CORNER_TOP_RIGHT;
466                    // fixing opposite corner
467                } else if (absTop > ((m_top + m_height) - offset)) {
468                    // bottom right corner
469                    return Area.CORNER_BOTTOM_RIGHT;
470                    // fixing opposite corner
471                }
472                return Area.BORDER_RIGHT;
473            }
474            if (absTop < (m_top + offset)) {
475                // border top
476                return Area.BORDER_TOP;
477            } else if (absTop > ((m_top + m_height) - offset)) {
478                // border bottom
479                return Area.BORDER_BOTTOM;
480            }
481            return Area.CENTER;
482        }
483        return null;
484    }
485
486    /**
487     * Returns the height.<p>
488     *
489     * @return the height
490     */
491    public int getHeight() {
492
493        return m_height;
494    }
495
496    /**
497     * Returns the left.<p>
498     *
499     * @return the left
500     */
501    public int getLeft() {
502
503        return m_left;
504    }
505
506    /**
507     * Returns the top.<p>
508     *
509     * @return the top
510     */
511    public int getTop() {
512
513        return m_top;
514    }
515
516    /**
517     * Returns the width.<p>
518     *
519     * @return the width
520     */
521    public int getWidth() {
522
523        return m_width;
524    }
525
526    /**
527     * Checks whether the given position is completely surrounded by this position.<p>
528     *
529     * @param child the child position
530     * @param padding the padding to use
531     *
532     * @return <code>true</code> if the child position is completely surrounded
533     */
534    public boolean isInside(CmsPositionBean child, int padding) {
535
536        return ((getLeft() + padding) < child.getLeft()) // checking left border
537            && ((getTop() + padding) < child.getTop()) // checking top border
538            && (((getLeft() + getWidth()) - padding) > (child.getLeft() + child.getWidth())) // checking right border
539            && (((getTop() + getHeight()) - padding) > (child.getTop() + child.getHeight())); // checking bottom border
540    }
541
542    /**
543     * Returns if given position is inside the position beans coordinates.<p>
544     *
545     * @param absLeft the absolute left position
546     * @param absTop the absolute top position
547     *
548     * @return true if the given position if within the beans coordinates
549     */
550    public boolean isOverElement(int absLeft, int absTop) {
551
552        if ((absTop > m_top) && (absTop < (m_top + m_height)) && (absLeft > m_left) && (absLeft < (m_left + m_width))) {
553            return true;
554        }
555        /*     */
556        return false;
557    }
558
559    /**
560     * Returns if given absolute top is above the vertical middle of the position beans coordinates.<p>
561     *
562     * @param absTop the absolute top position
563     * @return true if given absolute top is above the vertical middle
564     */
565    public boolean isOverTopHalf(int absTop) {
566
567        if (absTop < (m_top + (m_height / 2))) {
568            return true;
569        }
570        return false;
571    }
572
573    /**
574     * Sets the height.<p>
575     *
576     * @param height the height to set
577     */
578    public void setHeight(int height) {
579
580        m_height = height;
581    }
582
583    /**
584     * Sets the left.<p>
585     *
586     * @param left the left to set
587     */
588    public void setLeft(int left) {
589
590        m_left = left;
591    }
592
593    /**
594     * Sets the top.<p>
595     *
596     * @param top the top to set
597     */
598    public void setTop(int top) {
599
600        m_top = top;
601    }
602
603    /**
604     * Sets the width.<p>
605     *
606     * @param width the width to set
607     */
608    public void setWidth(int width) {
609
610        m_width = width;
611    }
612
613    /**
614     * @see java.lang.Object#toString()
615     */
616    @Override
617    public String toString() {
618
619        return "top: " + m_top + "   left: " + m_left + "   height: " + m_height + "   width: " + m_width;
620    }
621
622}