1

Like the title says, I need to see if two string locations intersect before drawing them with graphics2d. This way I don't have strings over each other so you can't read them.

Some details:

Screen size is 1000x1000 px. I am randomly generating coordinate locations and fonts at a fixed interval of 10 miliseconds. Then (also every 10 miliseconds) I use g2d.drawString() to draw the word "popup!" to the screen in my paintComponent() method with the random fonts and random locations I store previously. However, since I am randomly generating coordinates, this means that ocasionally I have the messages overlap. How can I ensure that this wont happen by either not allowing it to generate the coordinates that overlap or by not printing messages that overlap?

Code:

Font[] popups = new Font[20];
int[][] popupsLoc = new int[20][2];
Random rn = new Random();
public void addPopup() { //is being called every 10 miliseconds by timer 
    boolean needPopup = false;
        int where = 0;
        for(int i = 0; i < popups.length; i++) {
            if(popups[i] == null) {
                needPopup = true;
                where = i;
                }
            }
        if(needPopup == true) {
            popups[where] = new Font("STENCIL", Font.BOLD, rn.nextInt(100) + 10);
            popupsLoc[where][0] = rn.nextInt(800);
            popupsLoc[where][1] = rn.nextInt(800);
        }
    }
} //in paintComponent() I iterate through the popups[] array and draw the element with the font

Paint Code:

public void paintComponent(Graphics g) {
        super.paintComponent(g);

        setBackground(Color.BLACK);
        Graphics2D g2d = (Graphics2D) g;


        for(int i = 0; i < popups.length; i++) {
            if(popups[i] != null) {
                g2d.setColor(popupColor);
                g2d.setFont(popups[i]);
                g2d.drawString("Popup!", popupsLoc[i][0], popupsLoc[i][1]);
            }
        }
}

Example enter image description here

As you can see, two of the messages are overlapping here in the bottom right of the screen. How can I prevent that?

Edit: I have found a very simple solution.

public void addPopup() {

            boolean needPopup = false;
            int where = 0;
            for (int i = 0; i < popups.length; i++) {

                if (popups[i] == null) {
                    needPopup = true;
                    where = i;
                }
            }
            if (needPopup == true) {
                boolean doesIntersect = false;
                popups[where] = new Font("STENCIL", Font.BOLD, rn.nextInt(100) + 10);
                popupsLoc[where][0] = rn.nextInt(800);
                popupsLoc[where][1] = rn.nextInt(800);

                FontMetrics metrics = getFontMetrics(popups[where]);
                int hgt = metrics.getHeight();
                int wdh = metrics.stringWidth("Popup!");
                popupsHitbox[where] = new Rectangle(popupsLoc[where][0], popupsLoc[where][1], wdh, hgt);
                //System.out.println(hgt);

                for (int i = where + 1; i < popups.length; i++) {
                    if (popupsHitbox[i] != null) {
                        if (popupsHitbox[where].intersects(popupsHitbox[i]))
                            doesIntersect = true;

                    }
                }
                if (doesIntersect == true) {
                    popups[where] = null;
                    popupsLoc[where][0] = 0;
                    popupsLoc[where][1] = 0;
                    popupsHitbox[where] = null;
                    addPopup();
                }
            }

    }

Then when I draw:

for (int i = 0; i < popups.length; i++) {
            if (popups[i] != null) {
                g2d.setColor(popupColor);
                g2d.setFont(popups[i]);
                FontMetrics metrics = getFontMetrics(popups[i]);
                g2d.drawString("Popup!", popupsLoc[i][0], popupsLoc[i][1]+metrics.getHeight());
                //g2d.draw(popupsHitbox[i]);
            }
        }

The explanation is this: When I create a popup font/coord location, I also create a rectangle "hitbox" using the coord location and FontMetrics to get the size the message will be in pixels, then I store this rectangle to an array. After that, I have a boolean flag called doesIntersect which is initalized to false. I loop through all the hitboxes and check if the current one intersects() with any others. If so, I set the flag to true. Then, after it checks, if the flag is true it resets that location in the array to null and recalls addPopup(). (There could be some recursion here) Finally, when I paint I just draw the string at the coordinate location, (with y+height since strings paint from bottom left). May not be very clean, but it works.

Ashwin Gupta
  • 2,159
  • 9
  • 30
  • 60
  • Does this help? https://docs.oracle.com/javase/tutorial/2d/text/measuringtext.html – ajb Feb 03 '16 at 05:48
  • @ajb actually yes it does thanks. But it still doesn't answer it completly. I thought about specifying rectangle hitboxes when I create the random fonts and coord locations so that would let me do that, but then even with the .intersects method I have no way to check against all of the other fonts if there are overlaps. (sorry if that sounded super confusing. Short answer: yes, it helps, but still need more help!) – Ashwin Gupta Feb 03 '16 at 05:50
  • 1) Get a `Shape` for the text as seen in [this answer](http://stackoverflow.com/a/6296381/418556). 2) Check if the shapes intersect as seen in [this answer](http://stackoverflow.com/a/14575043/418556). – Andrew Thompson Feb 03 '16 at 06:06
  • @MadProgrammer I can't carefully read the duplicate ATM because its too late, I'll look tomorrow. But, from a quick glance, I think mines is a bit different because it has the further complication of a fixed array size and the order of the strings being generated/drawn. – Ashwin Gupta Feb 03 '16 at 06:06
  • @AndrewThompson thanks, I will check those out tomorrow. I'm guessing that someone here probably solved my problem, either you, emily, or mad programmer. – Ashwin Gupta Feb 03 '16 at 06:08
  • @AshwinGupta Conceptually, you're trying to determine if you can draw a string at a given location without intersecting other text ... basically the same thing, but take some time to look it over – MadProgrammer Feb 03 '16 at 06:08
  • @MadProgrammer I figured it out! A super simple way also. I just assigned a rectangle hitbox then iterated through the array of rectangles, if it intersected any rectangle, it returns a boolean value for the variables doesInteresct (which I initalize to false) then if the boolean value is true I recall the method addPopup();. If you want I can post the working code in the question body. I can't add an answer because of the duplicate mark. – Ashwin Gupta Feb 04 '16 at 00:20
  • @AndrewThompson I got it! See above comment ^. Thanks again for your help! – Ashwin Gupta Feb 04 '16 at 00:21
  • @AshwinGupta That's pretty much what the duplicate answer does – MadProgrammer Feb 04 '16 at 00:49
  • @MadProgrammer oh okay alright. Guess your duplicate was the right thing then. Thanks! – Ashwin Gupta Feb 04 '16 at 00:52

1 Answers1

0

I created a static utility class for generating accurate Shape instances for a given String and Graphics2D rendering surface which will efficiently calculate intersection detection without the error associated with only using bounding boxes.

/**
 * Provides methods for generating accurate shapes describing the area a particular {@link String} will occupy when
 * drawn alongside methods which can calculate the intersection of those shapes efficiently and accurately.
 * 
 * @author Emily Mabrey (emabrey@users.noreply.github.com)
 */

public class TextShapeIntersectionCalculator {

  /**
   * An {@link AffineTransform} which returns the given {@link Area} unchanged.
   */
  private static final AffineTransform NEW_AREA_COPY = new AffineTransform();

  /**
   * Calculates the delta between two single coordinate values.
   * 
   * @param coordinateA
   *        The origination coordinate which we are calculating from
   * @param coordinateB
   *        The destination coordinate which the delta takes us to
   * @return A coordinate value delta which expresses the change from A to B
   */
  private static int getCoordinateDelta(final int coordinateA, final int coordinateB) {

    return coordinateB - coordinateA;
  }

  /**
   * Calls {@link #getTextShape(TextLayout)} using a {@link TextLayout} generated from the provided objects and
   * returns the generated {@link Shape}.
   * 
   * @param graphicsContext
   *        A non-null {@link Graphics2D} object with the configuration of the desired drawing surface
   * @param string
   *        An {@link AttributedString} containing the data describing which characters to draw alongside the
   *        {@link Attribute Attributes} describing how those characters should be drawn.
   * @return A {@link Shape} generated via {@link #getTextShape(TextLayout)}
   */
  public static Shape getTextShape(final Graphics2D graphicsContext, final AttributedString string) {

    final FontRenderContext fontContext = graphicsContext.getFontRenderContext();

    final TextLayout textLayout = new TextLayout(string.getIterator(), fontContext);

    return getTextShape(textLayout);
  }

  /**
   * Calls {@link #getTextShape(TextLayout)} using a {@link TextLayout} generated from the provided objects and
   * returns the generated {@link Shape}.
   * 
   * @param graphicsContext
   *        A non-null {@link Graphics2D} object with the configuration of the desired drawing surface
   * @param attributes
   *        A non-null {@link Map} object populated with {@link Attribute} objects which will be used to determine the
   *        glyphs and styles for rendering the character data
   * @param string
   *        A {@link String} containing the character data which is to be drawn
   * @return A {@link Shape} generated via {@link #getTextShape(TextLayout)}
   */
  public static Shape getTextShape(final Graphics2D graphicsContext, final Map<? extends Attribute, ?> attributes,
    final String string) {

    final FontRenderContext fontContext = graphicsContext.getFontRenderContext();

    final TextLayout textLayout = new TextLayout(string, attributes, fontContext);

    return getTextShape(textLayout);
  }

  /**
   * Calls {@link #getTextShape(TextLayout)} using a {@link TextLayout} generated from the provided objects and
   * returns the generated {@link Shape}.
   * 
   * @param graphicsContext
   *        A non-null {@link Graphics2D} object with the configuration of the desired drawing surface
   * @param outputFont
   *        A non-null {@link Font} object used to determine the glyphs and styles for rendering the character data
   * @param string
   *        A {@link String} containing the character data which is to be drawn
   * @return A {@link Shape} generated via {@link #getTextShape(TextLayout)}
   */
  public static Shape getTextShape(final Graphics2D graphicsContext, final Font outputFont, final String string) {

    final FontRenderContext fontContext = graphicsContext.getFontRenderContext();

    final TextLayout textLayout = new TextLayout(string, outputFont, fontContext);

    return getTextShape(textLayout);
  }

  /**
   * Determines the {@link Shape} which should be generated by rendering the given {@link TextLayout} object using the
   * internal {@link Graphics2D} rendering state alongside the internal {@link String} and {@link Font}. The returned
   * {@link Shape} is a potentially disjoint union of all the glyph shapes generated from the character data. Note that
   * the states of the mutable contents of the {@link TextLayout}, such as {@link Graphics2D}, will not be modified.
   * 
   * @param textLayout
   *        A {@link TextLayout} with an available {@link Graphics2D} object
   * @return A {@link Shape} which is likely a series of disjoint polygons
   */
  public static Shape getTextShape(final TextLayout textLayout) {

    final int firstSequenceEndpoint = 0, secondSequenceEndpoint = textLayout.getCharacterCount();
    final Shape generatedCollisionShape = textLayout.getBlackBoxBounds(firstSequenceEndpoint, secondSequenceEndpoint);

    return generatedCollisionShape;

  }

  /**
   * Converts the absolute coordinates of {@link Shape Shapes} a and b into relative coordinates and uses the converted
   * coordinates to call and return the result of {@link #checkForIntersection(Shape, Shape, int, int)}.
   * 
   * @param a
   *        A shape located with a user space location
   * @param aX
   *        The x coordinate of {@link Shape} a
   * @param aY
   *        The y coordinate of {@link Shape} a
   * @param b
   *        A shape located with a user space location
   * @param bX
   *        The x coordinate of {@link Shape} b
   * @param bY
   *        The x coordinate of {@link Shape} b
   * @return True if the two shapes at the given locations intersect, false if they do not intersect.
   */
  public static boolean checkForIntersection(final Shape a, final int aX, final int aY, final Shape b, final int bX,
    final int bY) {

    return checkForIntersection(a, b, getCoordinateDelta(aX, bX), getCoordinateDelta(aY, bY));
  }

  /**
   * Detects if two shapes with relative user space locations intersect. The intersection is checked in a way which
   * fails quickly if there is no intersection and which succeeds using the least amount of calculation required to
   * determine there is an intersection. The location of {@link Shape} a is considered to be the origin and the position
   * of {@link Shape} b is defined relative to the position of {@link Shape} a using the provided coordinate deltas.
   * 
   * @param a
   *        The shape placed at what is considered the origin
   * @param b
   *        The shape placed in the position relative to a
   * @param relativeDeltaX
   *        The delta x coordinate of {@link Shape} b compared to the x coordination of {@link Shape} a (which is always
   *        0).
   * @param relativeDeltaY
   *        The delta y coordinate of {@link Shape} b compared to the y coordination of {@link Shape} a (which is always
   *        0).
   * @return True to indicate the provided {@link Shape Shapes} intersect when placed in the indicated locations.
   */
  public static boolean checkForIntersection(final Shape a, final Shape b, int relativeDeltaX, int relativeDeltaY) {

    return isIntersectionUsingSimpleBounds(a, b, relativeDeltaX, relativeDeltaY)
      && isIntersectionUsingAdvancedBounds(a, b, relativeDeltaX, relativeDeltaY)
      && isIntersectionUsingExactAreas(a, b, relativeDeltaX, relativeDeltaY);
  }

  /**
   * Detects if two shapes with relative user space locations intersect. The intersection is checked using a fast but
   * extremely simplified bounding box calculation. The location of {@link Shape} a is considered to be the origin and
   * the position of {@link Shape} b is defined relative to the position of {@link Shape} a using the provided
   * coordinate deltas.
   * 
   * @param a
   *        The shape placed at what is considered the origin
   * @param b
   *        The shape placed in the position relative to a
   * @param relativeDeltaX
   *        The delta x coordinate of {@link Shape} b compared to the x coordination of {@link Shape} a (which is always
   *        0).
   * @param relativeDeltaY
   *        The delta y coordinate of {@link Shape} b compared to the y coordination of {@link Shape} a (which is always
   *        0).
   * @return True to indicate the provided {@link Shape Shapes} intersect when placed in the indicated locations.
   */
  private static boolean isIntersectionUsingSimpleBounds(final Shape a, final Shape b, int relativeDeltaX,
    int relativeDeltaY) {

    final Rectangle rectA = a.getBounds();
    final Rectangle rectB = b.getBounds();

    rectB.setLocation(rectA.getLocation());
    rectB.translate(relativeDeltaX, relativeDeltaY);

    return rectA.contains(rectB);
  }

  /**
   * Detects if two shapes with relative user space locations intersect. The intersection is checked using a slightly
   * simplified bounding box calculation. The location of {@link Shape} a is considered to be the origin and the
   * position of {@link Shape} b is defined relative to the position of {@link Shape} a using the provided coordinate
   * deltas.
   * 
   * @param a
   *        The shape placed at what is considered the origin
   * @param b
   *        The shape placed in the position relative to a
   * @param relativeDeltaX
   *        The delta x coordinate of {@link Shape} b compared to the x coordination of {@link Shape} a (which is always
   *        0).
   * @param relativeDeltaY
   *        The delta y coordinate of {@link Shape} b compared to the y coordination of {@link Shape} a (which is always
   *        0).
   * @return True to indicate the provided {@link Shape Shapes} intersect when placed in the indicated locations.
   */
  private static boolean isIntersectionUsingAdvancedBounds(final Shape a, final Shape b, int relativeDeltaX,
    int relativeDeltaY) {

    final Rectangle2D rectA = a.getBounds();
    final Rectangle2D rectB = b.getBounds();

    rectB.setRect(rectA.getX() + relativeDeltaX, rectA.getY() + relativeDeltaY, rectB.getWidth(), rectB.getHeight());

    return rectA.contains(rectB);
  }

  /**
   * Detects if two shapes with relative user space locations intersect. The intersection is checked using a slow but
   * perfectly accurate calculation. The location of {@link Shape} a is considered to be the origin and the position of
   * {@link Shape} b is defined relative to the position of {@link Shape} a using the provided coordinate deltas.
   * 
   * @param a
   *        The shape placed at what is considered the origin
   * @param b
   *        The shape placed in the position relative to a
   * @param relativeDeltaX
   *        The delta x coordinate of {@link Shape} b compared to the x coordination of {@link Shape} a (which is always
   *        0).
   * @param relativeDeltaY
   *        The delta y coordinate of {@link Shape} b compared to the y coordination of {@link Shape} a (which is always
   *        0).
   * @return True to indicate the provided {@link Shape Shapes} intersect when placed in the indicated locations.
   */
  private static boolean isIntersectionUsingExactAreas(final Shape a, final Shape b, int relativeDeltaX,
    int relativeDeltaY) {

    final Area aClone = new Area(a).createTransformedArea(NEW_AREA_COPY);
    final Area bClone = new Area(b).createTransformedArea(NEW_AREA_COPY);

    bClone.transform(AffineTransform.getTranslateInstance(relativeDeltaX, relativeDeltaY));
    aClone.intersect(bClone);

    return !aClone.isEmpty();
  }

}

Using this class you should be able to draw a String anywhere an actual character glyph isn't, even if the place you want to draw is inside a bounding box for another String.

I rewrote the code you gave me to use my new intersection detection, but while rewriting it I cleaned it up and added some new classes to improve it. These two classes are simply data structures and they are needed alongside my rewrite of your code:

class StringDrawInformation {

    public StringDrawInformation(final String s, final Font f, final Color c, final int x, final int y) {
      this.text = s;
      this.font = f;
      this.color = c;
      this.x = x;
      this.y = y;
    }

    public final String text;

    public final Font font;

    public final Color color;

    public int x, y;
  }

class DrawShape {

    public DrawShape(final Shape s, final StringDrawInformation drawInfo) {
      this.shape = s;
      this.drawInfo = drawInfo;
    }

    public final Shape shape;

    public StringDrawInformation drawInfo;
  }

Using my three new classes I rewrote your code to look like this:

  private static final Random random = new Random();

  public static final List<StringDrawInformation> generateRandomDrawInformation(int newCount) {

    ArrayList<StringDrawInformation> newInfos = new ArrayList<>();

    for (int i = 0; newCount > i; i++) {
      String s = "Popup!";
      Font f = new Font("STENCIL", Font.BOLD, random.nextInt(100) + 10);
      Color c = Color.WHITE;
      int x = random.nextInt(800);
      int y = random.nextInt(800);
      newInfos.add(new StringDrawInformation(s, f, c, x, y));
    }

    return newInfos;
  }

  public static List<DrawShape> generateRenderablePopups(final List<StringDrawInformation> in, Graphics2D g2d) {

    List<DrawShape> outShapes = new ArrayList<>();

    for (StringDrawInformation currentInfo : in) {
      Shape currentShape = TextShapeIntersectionCalculator.getTextShape(g2d, currentInfo.font, currentInfo.text);
      boolean placeIntoOut = true;

      for (DrawShape nextOutShape : outShapes) {
        if (TextShapeIntersectionCalculator.checkForIntersection(nextOutShape.shape, nextOutShape.drawInfo.x,
          nextOutShape.drawInfo.y, currentShape, currentInfo.x, currentInfo.y)) {
          // we found an intersection so we dont place into out and we stop verifying
          placeIntoOut = false;
          break;
        }
      }

      if (placeIntoOut) {
        outShapes.add(new DrawShape(currentShape, currentInfo));
      }
    }

    return outShapes;

  }

  private List<StringDrawInformation> popups = generateRandomDrawInformation(20);

  public void paintComponent(Graphics g) {

    super.paintComponent(g);
    Graphics2D g2d = (Graphics2D) g;
    g2d.setBackground(Color.BLACK);

    for (DrawShape renderablePopup : generateRenderablePopups(popups, g2d)) {
      g2d.setColor(renderablePopup.drawInfo.color);
      g2d.setFont(renderablePopup.drawInfo.font);
      g2d.drawString(renderablePopup.drawInfo.text, renderablePopup.drawInfo.x, renderablePopup.drawInfo.y);
    }
  }

The rewritten code is easily modified to use more shapes, different fonts, different colors, etc. instead of being extremely hard to modify. I wrapped the different data into super-types which encapsulated the smaller data types to make them easier to use. My rewrite is no means perfect, but hopefully this helps.

I haven't actually tested this code yet, just wrote it by hand. So hopefully it works as intended. I will get around to testing it eventually, it was hard enough to find time to write what I already accomplished. If you have any question feel free to ask them. Sorry it took me so long to make my answer!

Edit: a small afterthought - the order of the StringDrawInformation List passed to generateRenderabePopups(...) is in order of priority. Each list element is compared against all currently validated elements. The first unchecked element always validates successfully, because there are no comparisons. The 2nd unchecked element is checked against the 1st because the 1st was validated. The 3rd unchecked may be checked against up to 2 other elements, the 4th up to 3. Basically, an element in position i might be checked against i-1 other elements. So, if it matters, put the more important text earlier in the list and the least important text later in the list.

Emily Mabrey
  • 1,528
  • 1
  • 12
  • 29
  • Okay, I added my paint code to question body. It's getting late here so I have to get to sleep but I'll check back here tmrw morning 6:50 ish. – Ashwin Gupta Feb 03 '16 at 06:05
  • The pixel size of text is based on the `Font` and the `Graphics` context onto which it is to be drawn. Much of the functionality is contained within the `Graphics` API itself, for more details see [Working with Text APIs](https://docs.oracle.com/javase/tutorial/2d/text/index.html) – MadProgrammer Feb 03 '16 at 06:06
  • Trying to write an instance of `Composite` would be the most efficient way to implement this code, but the way the JDK uses that class, alongside `CompositeContext`, `Blit` and others is super complicated. I am going to try to get a working implementation of my original idea, but if I fail/it is too complicated, I will provide an answer involving a less efficient and more brutish implementation based upon intersection detection. – Emily Mabrey Feb 03 '16 at 06:46
  • The above posted "previous answers" work the simple way I described before. Unfortunately, since that method relies on a bounding rectangle, it doesn't actually accurately determine if text is overlapping. The parts of the bounding rectangle which contain no text can still prevent another `Font` graphic from being written. My method using a `Composite` instance will correctly determine if two `Font` graphics intersect without that inaccuracy, however it is so complicated to create that I am still writing it. If I get the thing written I will post it even if you accept another answer first. – Emily Mabrey Feb 03 '16 at 11:57
  • @EmilyM wow I didn't think this was going to be that complex. I hope I haven't wasted too much of your time. Sorry for all the trouble :(! If you can't no stress, I can figure out another way or try the duplicate. I will do it after school today. By the way, I don't care about a little bit of inaccuracy, if it prevents other text popups from spawning within like a 10-20 pixel inaccuracy thats okay. The program I'm working on is just for fun and so I could learn so if its a bit messy that's just fine. – Ashwin Gupta Feb 03 '16 at 14:34
  • @EmilyM I think I got it! See the comment I wrote to madprogrammer on the question body. Thanks for your help. – Ashwin Gupta Feb 04 '16 at 00:20
  • @EmilyM Which is why I also suggested using `TextLayout`, which creates a tighter `Shape` of the text and linked to [this answer]([Worker Threads and SwingWorker](http://docs.oracle.com/javase/tutorial/uiswing/concurrency/worker.html)) as an example ;) – MadProgrammer Feb 04 '16 at 00:50