001/*
002 * Copyright 2010-2015 Institut Pasteur.
003 * 
004 * This file is part of Icy.
005 * 
006 * Icy is free software: you can redistribute it and/or modify
007 * it under the terms of the GNU General Public License as published by
008 * the Free Software Foundation, either version 3 of the License, or
009 * (at your option) any later version.
010 * 
011 * Icy is distributed in the hope that it will be useful,
012 * but WITHOUT ANY WARRANTY; without even the implied warranty of
013 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
014 * GNU General Public License for more details.
015 * 
016 * You should have received a copy of the GNU General Public License
017 * along with Icy. If not, see <http://www.gnu.org/licenses/>.
018 */
019package icy.util;
020
021import icy.image.ImageUtil;
022import icy.util.ShapeUtil.ShapeConsumer;
023
024import java.awt.AlphaComposite;
025import java.awt.BasicStroke;
026import java.awt.Color;
027import java.awt.Component;
028import java.awt.Composite;
029import java.awt.Dimension;
030import java.awt.Font;
031import java.awt.FontMetrics;
032import java.awt.GradientPaint;
033import java.awt.Graphics;
034import java.awt.Graphics2D;
035import java.awt.Image;
036import java.awt.Point;
037import java.awt.Rectangle;
038import java.awt.RenderingHints;
039import java.awt.Shape;
040import java.awt.geom.PathIterator;
041import java.awt.geom.Rectangle2D;
042import java.awt.geom.RoundRectangle2D;
043
044/**
045 * Graphics utilities class.
046 * 
047 * @author Stephane
048 */
049public class GraphicsUtil
050{
051    public static float getAlpha(Graphics2D g)
052    {
053        final Composite composite = g.getComposite();
054
055        if (composite instanceof AlphaComposite)
056            return ((AlphaComposite) composite).getAlpha();
057
058        return 1f;
059    }
060
061    public static void mixAlpha(Graphics2D g, float factor)
062    {
063        mixAlpha(g, 0, factor);
064    }
065
066    public static float mixAlpha(Graphics2D g, int rule, float factor)
067    {
068        final Composite composite = g.getComposite();
069
070        if (composite instanceof AlphaComposite)
071        {
072            final AlphaComposite alphaComposite = (AlphaComposite) composite;
073            final float alpha = Math.min(1f, Math.max(0f, alphaComposite.getAlpha() * factor));
074
075            if (rule == 0)
076                g.setComposite(AlphaComposite.getInstance(alphaComposite.getRule(), alpha));
077            else
078                g.setComposite(AlphaComposite.getInstance(rule, alpha));
079
080            return alpha;
081        }
082
083        g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, factor));
084
085        return factor;
086    }
087
088    /**
089     * Draw ICY style background on the specified Graphics object with specified dimension.
090     */
091    public static void paintIcyBackGround(int width, int height, Graphics g)
092    {
093        final Graphics2D g2 = (Graphics2D) g.create();
094
095        final float ray = Math.max(width, height) * 0.05f;
096        final RoundRectangle2D roundRect = new RoundRectangle2D.Double(0, 0, width, height, Math.min(ray * 2, 20),
097                Math.min(ray * 2, 20));
098
099        g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
100
101        g2.setPaint(new GradientPaint(0, 0, Color.white.darker(), 0, height / 1.5f, Color.black));
102        g2.fill(roundRect);
103
104        g2.setPaint(Color.black);
105        g2.setColor(Color.black);
106        mixAlpha(g2, AlphaComposite.SRC_OVER, 1f / 3f);
107        // g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.3f));
108        g2.fillOval(-width + (width / 2), height / 2, width * 2, height);
109
110        mixAlpha(g2, AlphaComposite.SRC_OVER, 3f / 1f);
111        // g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 1f));
112        g2.setStroke(new BasicStroke(Math.max(1f, Math.min(5f, ray))));
113        g2.draw(roundRect);
114
115        g2.dispose();
116    }
117
118    /**
119     * Draw ICY style background on the specified Graphics object with specified component
120     * dimension.
121     */
122    public static void paintIcyBackGround(Component component, Graphics g)
123    {
124        paintIcyBackGround(component.getWidth(), component.getHeight(), g);
125    }
126
127    /**
128     * Draw ICY style background in the specified Image
129     */
130    public static void paintIcyBackGround(Image image)
131    {
132        final Graphics g = image.getGraphics();
133        // be sure image data are ready
134        ImageUtil.waitImageReady(image);
135        // draw background in image
136        paintIcyBackGround(image.getWidth(null), image.getHeight(null), g);
137        g.dispose();
138    }
139
140    /**
141     * Returns true if the specified region is visible in the specified {@link Graphics} object.<br>
142     * Internally use the {@link Graphics} clip area to determine if region is visible.
143     */
144    public static boolean isVisible(Graphics g, Rectangle region)
145    {
146        if ((g == null) || (region == null))
147            return false;
148
149        final Rectangle clipRegion = g.getClipBounds();
150
151        // no clip region --> return true
152        if (clipRegion == null)
153            return true;
154
155        if (region.width == 0)
156        {
157            // special case of single point region
158            if (region.height == 0)
159                return clipRegion.contains(region.x, region.y);
160            else
161                // special case of null width region
162                return clipRegion.contains(region.x, region.getMinY())
163                        || clipRegion.contains(region.x, region.getMaxY());
164
165        }
166        else if (region.height == 0)
167            // special case of null height region
168            return clipRegion.contains(region.getMinX(), region.y) || clipRegion.contains(region.getMaxX(), region.y);
169        else
170            return clipRegion.intersects(region);
171    }
172
173    /**
174     * Returns true if the specified region is visible in the specified {@link Graphics} object.<br>
175     * Internally use the {@link Graphics} clip area to determine if region is visible.
176     */
177    public static boolean isVisible(Graphics g, Rectangle2D region)
178    {
179        if ((g == null) || (region == null))
180            return false;
181
182        final Rectangle clipRegion = g.getClipBounds();
183
184        // no clip region --> return true
185        if (clipRegion == null)
186            return true;
187
188        if (region.getWidth() == 0d)
189        {
190            // special case of single point region
191            if (region.getHeight() == 0d)
192                return clipRegion.contains(region.getX(), region.getY());
193
194            // special case of null width region
195            return clipRegion.contains(region.getX(), region.getMinY())
196                    || clipRegion.contains(region.getX(), region.getMaxY());
197        }
198        else if (region.getHeight() == 0d)
199            // special case of null height region
200            return clipRegion.contains(region.getMinX(), region.getY())
201                    || clipRegion.contains(region.getMaxX(), region.getY());
202        else
203            return clipRegion.intersects(region);
204    }
205
206    /**
207     * Returns bounds to draw specified string in the specified Graphics context
208     * with specified font.<br>
209     * This function handle multi lines string ('\n' character used a line separator).
210     */
211    public static Rectangle2D getStringBounds(Graphics g, Font f, String text)
212    {
213        Rectangle2D result = new Rectangle2D.Double();
214
215        if (g != null)
216        {
217            final FontMetrics fm;
218
219            if (f == null)
220                fm = g.getFontMetrics();
221            else
222                fm = g.getFontMetrics(f);
223
224            for (String s : text.split("\n"))
225            {
226                final Rectangle2D r = fm.getStringBounds(s, g);
227
228                if (result.isEmpty())
229                    result = r;
230                else
231                    result.setRect(result.getX(), result.getY(), Math.max(result.getWidth(), r.getWidth()),
232                            result.getHeight() + r.getHeight());
233            }
234        }
235
236        return result;
237    }
238
239    /**
240     * Limit the single line string so it fits in the specified component width.
241     */
242    public static String limitStringFor(Component c, String text, int width)
243    {
244        if (width <= 0)
245            return "";
246
247        final int w = width - 20;
248
249        if (w <= 0)
250            return "..";
251
252        String str = text;
253        int textWidth = (int) getStringBounds(c, str).getWidth();
254        boolean changed = false;
255
256        while (textWidth > w)
257        {
258            str = str.substring(0, (str.length() * w) / textWidth);
259            textWidth = (int) getStringBounds(c, str).getWidth();
260            changed = true;
261        }
262
263        if (changed)
264            return str.trim() + "...";
265
266        return text;
267    }
268
269    /**
270     * Return bounds to draw specified string in the specified component.<br>
271     * This function handle multi lines string ('\n' character used a line separator).
272     */
273    public static Rectangle2D getStringBounds(Component c, String text)
274    {
275        return getStringBounds(c.getGraphics(), text);
276    }
277
278    /**
279     * Return bounds to draw specified string in the specified Graphics context
280     * with current font.<br>
281     * This function handle multi lines string ('\n' character used a line separator).
282     */
283    public static Rectangle2D getStringBounds(Graphics g, String text)
284    {
285        return getStringBounds(g, null, text);
286    }
287
288    /**
289     * Draw a text in the specified Graphics context and at the specified position.<br>
290     * This function handles multi lines string ('\n' character used a line separator).
291     */
292    public static void drawString(Graphics g, String text, int x, int y, boolean shadow)
293    {
294        if (StringUtil.isEmpty(text))
295            return;
296
297        final Color color = g.getColor();
298        final Color shadowColor;
299        if (ColorUtil.getLuminance(color) > 128)
300            shadowColor = ColorUtil.sub(color, Color.gray);
301        else
302            shadowColor = ColorUtil.add(color, Color.gray);
303        final Rectangle2D textRect = getStringBounds(g, "M");
304
305        // get height for a single line of text
306        final double lineH = textRect.getHeight();
307        final int curX = (int) (x - textRect.getX());
308        double curY = y - textRect.getY();
309
310        for (String s : text.split("\n"))
311        {
312            if (shadow)
313            {
314                g.setColor(shadowColor);
315                g.drawString(s, curX + 1, (int) (curY + 1));
316                g.setColor(color);
317            }
318            g.drawString(s, curX, (int) curY);
319            curY += lineH;
320        }
321    }
322
323    /**
324     * Draw a horizontal centered text on specified position.
325     * This function handle multi lines string ('\n' character used a line separator).
326     */
327    public static void drawHCenteredString(Graphics g, String text, int x, int y, boolean shadow)
328    {
329        if (StringUtil.isEmpty(text))
330            return;
331
332        final Color color = g.getColor();
333        final Color shadowColor = ColorUtil.mix(color, Color.black);
334        final Rectangle2D textRect = getStringBounds(g, "M");
335
336        final double offX = textRect.getX();
337        double curY = y - textRect.getY();
338
339        for (String s : text.split("\n"))
340        {
341            final Rectangle2D r = getStringBounds(g, s);
342            final int curX = (int) (x - (offX + (r.getWidth() / 2)));
343
344            if (shadow)
345            {
346                g.setColor(shadowColor);
347                g.drawString(s, curX + 1, (int) (curY + 1));
348                g.setColor(color);
349            }
350            g.drawString(s, curX, (int) curY);
351            curY += r.getHeight();
352        }
353    }
354
355    /**
356     * Draw a horizontal and vertical centered text on specified position.
357     * This function handle multi lines string ('\n' character used a line separator).
358     */
359    public static void drawCenteredString(Graphics g, String text, int x, int y, boolean shadow)
360    {
361        if (StringUtil.isEmpty(text))
362            return;
363
364        final Color color = g.getColor();
365        final Color shadowColor = ColorUtil.mix(color, Color.black);
366        final Rectangle2D textRect = getStringBounds(g, text);
367
368        final double offX = textRect.getX();
369        double curY = y - (textRect.getY() + (textRect.getHeight() / 2));
370
371        for (String s : text.split("\n"))
372        {
373            final Rectangle2D r = getStringBounds(g, s);
374            final int curX = (int) (x - (offX + (r.getWidth() / 2)));
375
376            if (shadow)
377            {
378                g.setColor(shadowColor);
379                g.drawString(s, curX + 1, (int) (curY + 1));
380                g.setColor(color);
381            }
382            g.drawString(s, curX, (int) curY);
383            curY += r.getHeight();
384        }
385    }
386
387    /**
388     * Returns the size to draw a Hint type text in the specified Graphics context.
389     */
390    public static Dimension getHintSize(Graphics2D g, String text)
391    {
392        final Rectangle2D stringRect = getStringBounds(g, text);
393        return new Dimension((int) Math.ceil(stringRect.getWidth() + 10d), (int) Math.ceil(stringRect.getHeight() + 8d));
394    }
395
396    /**
397     * Returns the bounds to draw a Hint type text in the specified Graphics context<br>
398     * at the specified location.
399     */
400    public static Rectangle getHintBounds(Graphics2D g, String text, int x, int y)
401    {
402        final Dimension dim = getHintSize(g, text);
403        return new Rectangle(x, y, dim.width, dim.height);
404    }
405
406    /**
407     * Draw multi line Hint type text in the specified Graphics context<br>
408     * at the specified location.
409     */
410    public static void drawHint(Graphics2D g, String text, int x, int y, Color bgColor, Color textColor)
411    {
412        final Graphics2D g2 = (Graphics2D) g.create();
413
414        final Rectangle2D stringRect = getStringBounds(g, text);
415        // calculate hint rect
416        final RoundRectangle2D backgroundRect = new RoundRectangle2D.Double(x, y, (int) (stringRect.getWidth() + 10),
417                (int) (stringRect.getHeight() + 8), 8, 8);
418
419        g2.setStroke(new BasicStroke(1.3f));
420        // draw translucent background
421        g2.setColor(bgColor);
422        mixAlpha(g2, AlphaComposite.SRC_OVER, 1f / 2f);
423        // g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.5f));
424        g2.fill(backgroundRect);
425        // draw background border
426        g2.setColor(ColorUtil.mix(bgColor, Color.black));
427        mixAlpha(g2, AlphaComposite.SRC_OVER, 2f / 1f);
428        // g2.setComposite(AlphaComposite.Src);
429        g2.draw(backgroundRect);
430        // draw text
431        g2.setColor(textColor);
432        drawString(g2, text, x + 5, y + 4, false);
433
434        g2.dispose();
435    }
436
437    /**
438     * Returns the bounds to draw specified box dimension at specified position.<br>
439     * By default the draw process is done from specified position to right/bottom.
440     * 
441     * @param origin
442     *        the initial desired position we want to draw the box.
443     * @param dim
444     *        box dimension
445     * @param xSpace
446     *        horizontal space between the position and box
447     * @param ySpace
448     *        vertical space between the position and box
449     * @param top
450     *        box is at top of position
451     * @param left
452     *        box is at left of position
453     */
454    public static Rectangle getBounds(Point origin, Dimension dim, int xSpace, int ySpace, boolean top, boolean left)
455    {
456        int x = origin.x;
457        int y = origin.y;
458
459        if (left)
460            x -= dim.width + xSpace;
461        else
462            x += xSpace;
463        if (top)
464            y -= dim.height + ySpace;
465        else
466            y += ySpace;
467
468        return new Rectangle(x, y, dim.width, dim.height);
469    }
470
471    /**
472     * Returns the bounds to draw specified box dimension at specified position.
473     * By default the draw process is done from specified position to right/bottom.
474     * 
475     * @param origin
476     *        the initial desired position we want to draw the box.
477     * @param dim
478     *        box dimension
479     * @param top
480     *        box is at top of position
481     * @param left
482     *        box is at left of position
483     */
484    public static Rectangle getBounds(Point origin, Dimension dim, boolean top, boolean left)
485    {
486        return getBounds(origin, dim, 0, 0, top, left);
487    }
488
489    /**
490     * Returns the best position to draw specified box in specified region with initial desired
491     * position.
492     * 
493     * @param origin
494     *        the initial desired position we want to draw the box.
495     * @param dim
496     *        box dimension
497     * @param region
498     *        the rectangle defining the region where we want to draw
499     * @param xSpace
500     *        horizontal space between the position and box
501     * @param ySpace
502     *        vertical space between the position and box
503     * @param top
504     *        by default box is at top of position
505     * @param left
506     *        by default box is at left of position
507     */
508    public static Point getBestPosition(Point origin, Dimension dim, Rectangle region, int xSpace, int ySpace,
509            boolean top, boolean left)
510    {
511        final Rectangle bounds1 = getBounds(origin, dim, xSpace, ySpace, top, left);
512        if (region.contains(bounds1))
513            return new Point(bounds1.x, bounds1.y);
514
515        final Rectangle bounds2 = getBounds(origin, dim, xSpace, ySpace, top, !left);
516        if (region.contains(bounds2))
517            return new Point(bounds2.x, bounds2.y);
518
519        final Rectangle bounds3 = getBounds(origin, dim, xSpace, ySpace, !top, left);
520        if (region.contains(bounds3))
521            return new Point(bounds3.x, bounds3.y);
522
523        final Rectangle bounds4 = getBounds(origin, dim, xSpace, ySpace, !top, !left);
524        if (region.contains(bounds4))
525            return new Point(bounds4.x, bounds4.y);
526
527        Rectangle r;
528
529        r = region.intersection(bounds1);
530        final long size1 = r.width * r.height;
531        r = region.intersection(bounds2);
532        final long size2 = r.width * r.height;
533        r = region.intersection(bounds3);
534        final long size3 = r.width * r.height;
535        r = region.intersection(bounds4);
536        final long size4 = r.width * r.height;
537
538        long maxSize = Math.max(size1, Math.max(size2, Math.max(size3, size4)));
539
540        if (maxSize == size1)
541            return new Point(bounds1.x, bounds1.y);
542        if (maxSize == size2)
543            return new Point(bounds2.x, bounds2.y);
544        if (maxSize == size3)
545            return new Point(bounds3.x, bounds3.y);
546
547        return new Point(bounds4.x, bounds4.y);
548    }
549
550    /**
551     * Returns the best position to draw specified box in specified region with initial desired
552     * position.
553     * 
554     * @param origin
555     *        the initial desired position we want to draw the box.
556     * @param dim
557     *        box dimension
558     * @param region
559     *        the rectangle defining the region where we want to draw
560     * @param xSpace
561     *        horizontal space between the position and box
562     * @param ySpace
563     *        vertical space between the position and box
564     */
565    public static Point getBestPosition(Point origin, Dimension dim, Rectangle region, int xSpace, int ySpace)
566    {
567        return getBestPosition(origin, dim, region, xSpace, ySpace, false, false);
568    }
569
570    /**
571     * Returns the best position to draw specified box in specified region with initial desired
572     * position.
573     * 
574     * @param origin
575     *        the initial desired position we want to draw the box.
576     * @param dim
577     *        box dimension
578     * @param region
579     *        the rectangle defining the region where we want to draw
580     */
581    public static Point getBestPosition(Point origin, Dimension dim, Rectangle region)
582    {
583        return getBestPosition(origin, dim, region, 0, 0, false, false);
584    }
585
586    /**
587     * Draw the specified PathIterator in the specified Graphics2D context
588     */
589    public static void drawPathIterator(PathIterator path, final Graphics2D g)
590    {
591        ShapeUtil.consumeShapeFromPath(path, new ShapeConsumer()
592        {
593            @Override
594            public boolean consume(Shape shape)
595            {
596                // draw shape
597                g.draw(shape);
598                return true;
599            }
600        });
601    }
602
603}