How to get 'envelope' from layout GraphicElement

1487
5
Jump to solution
02-22-2020 08:43 AM
BrennanJohnson
New Contributor II

I am trying to programmatically determine the bounds (in layout/page coordinates) of a layout element. I see that there are methods like GetX(), GetWidth(), GetY(), GetHeight() on a GraphicElement, but the graphic element may not actually be positioned within (x,y) through (x+width, y+height). For instance, if you create a text graphic element using a CIMTextSymbol whose rotation is set to 90°, the reported width of the resulting element will be greater than the reported height, even though the element is rotated such that it is much taller than it is wide. These methods seem to be returning the size of the element before rotation is applied. I am hoping to get the actual bounds of the element within the page, much like you could get the envelope of a polygon within a map. I could probably use GetRotation() and GetAnchor() along with the X, Y, width, and height to calculate the actual bounds of the element, but I don't trust that rotation is the only other variable that is affecting the actual position and size of the element in the layout. Is there a convenient way to get the 'envelope' for any given GraphicElement in a layout?

Thanks,

Brennan

0 Kudos
1 Solution

Accepted Solutions
UmaHarano
Esri Regular Contributor

Hi,

The best way to get the bounding envelope of the graphic element is to:

* Construct a polygon using the X, Y, Width and Height of the element.

* Rotate the polygon with the element's rotation angle.

* Use the "Extent" property on the polygon to get the "envelope" extent.  

Here is some code that does this -

QueuedTask.Run( () => {
        var layout = Project.Current.GetItems<LayoutProjectItem>().FirstOrDefault().GetLayout();
        var element = layout.Elements.OfType<GraphicElement>().FirstOrDefault();
        if (element == null) return;
        //Get the element's X, Y, Width and height
        var x = element.GetX();
        var y = element.GetY();
        var width = element.GetWidth();
        var height = element.GetHeight();

        //Create a polygon using the Graphic elements X, Y, Width and Height.
        MapPoint pt1 = MapPointBuilder.CreateMapPoint(x, y); //Anchor pt - lower left
        MapPoint pt2 = MapPointBuilder.CreateMapPoint(x + width, y); //lower right
        MapPoint pt3 = MapPointBuilder.CreateMapPoint(x + width, y + height );//upper right
        MapPoint pt4 = MapPointBuilder.CreateMapPoint(x, y + height);//upper left

        List<MapPoint> list = new List<MapPoint>() { pt1, pt2, pt3, pt4 };

        Polygon polygon = PolygonBuilder.CreatePolygon(list, null);

        //Rotate the polygon by the same angle the graphic element is rotated by
        var polygonRotate = GeometryEngine.Instance.Rotate(polygon, pt1, element.GetRotation() * (Math.PI / 180)) as Polygon; //Angle should be in radians.

        //Use polygonRotate to get the bounds of the rotated text element
        System.Diagnostics.Debug.WriteLine($"Width: {polygonRotate.Extent.Width}, Height: {polygonRotate.Extent.Height}");

        //Create another graphic element that envelops the original element
        Envelope env = polygonRotate.Extent;
        CIMStroke lineStroke = SymbolFactory.Instance.ConstructStroke(ColorFactory.Instance.BlackRGB, 1, SimpleLineStyle.DashDotDot);
        LayoutElementFactory.Instance.CreatePolygonGraphicElement(layout, env, SymbolFactory.Instance.ConstructPolygonSymbol(ColorFactory.Instance.RedRGB, SimpleFillStyle.Null, lineStroke));

      });

View solution in original post

5 Replies
UmaHarano
Esri Regular Contributor

Hi,

The best way to get the bounding envelope of the graphic element is to:

* Construct a polygon using the X, Y, Width and Height of the element.

* Rotate the polygon with the element's rotation angle.

* Use the "Extent" property on the polygon to get the "envelope" extent.  

Here is some code that does this -

QueuedTask.Run( () => {
        var layout = Project.Current.GetItems<LayoutProjectItem>().FirstOrDefault().GetLayout();
        var element = layout.Elements.OfType<GraphicElement>().FirstOrDefault();
        if (element == null) return;
        //Get the element's X, Y, Width and height
        var x = element.GetX();
        var y = element.GetY();
        var width = element.GetWidth();
        var height = element.GetHeight();

        //Create a polygon using the Graphic elements X, Y, Width and Height.
        MapPoint pt1 = MapPointBuilder.CreateMapPoint(x, y); //Anchor pt - lower left
        MapPoint pt2 = MapPointBuilder.CreateMapPoint(x + width, y); //lower right
        MapPoint pt3 = MapPointBuilder.CreateMapPoint(x + width, y + height );//upper right
        MapPoint pt4 = MapPointBuilder.CreateMapPoint(x, y + height);//upper left

        List<MapPoint> list = new List<MapPoint>() { pt1, pt2, pt3, pt4 };

        Polygon polygon = PolygonBuilder.CreatePolygon(list, null);

        //Rotate the polygon by the same angle the graphic element is rotated by
        var polygonRotate = GeometryEngine.Instance.Rotate(polygon, pt1, element.GetRotation() * (Math.PI / 180)) as Polygon; //Angle should be in radians.

        //Use polygonRotate to get the bounds of the rotated text element
        System.Diagnostics.Debug.WriteLine($"Width: {polygonRotate.Extent.Width}, Height: {polygonRotate.Extent.Height}");

        //Create another graphic element that envelops the original element
        Envelope env = polygonRotate.Extent;
        CIMStroke lineStroke = SymbolFactory.Instance.ConstructStroke(ColorFactory.Instance.BlackRGB, 1, SimpleLineStyle.DashDotDot);
        LayoutElementFactory.Instance.CreatePolygonGraphicElement(layout, env, SymbolFactory.Instance.ConstructPolygonSymbol(ColorFactory.Instance.RedRGB, SimpleFillStyle.Null, lineStroke));

      });
JeffBarrette
Esri Regular Contributor

Hello Brennan,

This was a great question and certainly generated some good discussion within our team.  May I ask why you want the "actual bounds of the element within the page"?  My assumption is that you want to position the element relative to other elements or other coordinates on the page.  If we were to build a helper function that required less gymnastics that the code above, what is it exactly you need?  Is it simply a polygon/extent?  Maybe a r/o .getPageEnvelope(). Or is it a list of x/y min/max values you need to perform the appropriate shifts on the page.  Below is an alternative script that does something very similar to the code above.  I don't need to create a geometry if I have the stats below to perform my positional adjustments.  But equally difficult is shifting an element on the layout based on the anchor position.  Granted the math is easier but there are 8 positions to take into account.

Again, if you could help recommend the exact function that would help with your workflow we might be able to add that capability into a future API.

Jeff - Layout Team

      Layout layout = LayoutView.Active.Layout;
      await QueuedTask.Run(() =>
      {
        //Reference text element and preserve original settings
        TextElement txtElm = layout.FindElement("Text") as TextElement;
        Double xMin = txtElm.GetX(), xMax = txtElm.GetX();
        Double yMin = txtElm.GetY(), yMax = txtElm.GetY();
        Anchor orig_anchor = txtElm.GetAnchor();

        //Iterate through each anchor position and preserve min/max values
        var values = Enum.GetValues(typeof(Anchor));
        foreach (Anchor val in values)
        {
          txtElm.SetAnchor(val);
          if (txtElm.GetX() < xMin) { xMin = txtElm.GetX(); }
          if (txtElm.GetY() < yMin) { yMin = txtElm.GetY(); }
          if (txtElm.GetX() > xMax) { xMax = txtElm.GetX(); }
          if (txtElm.GetY() > yMax) { yMax = txtElm.GetY(); }
        }
        txtElm.SetAnchor(orig_anchor);  //Need to set back to original location

        //The following just creates a visual representation of the graphic
        Envelope env = EnvelopeBuilder.CreateEnvelope(xMin, yMin, xMax, yMax, null);
        CIMStroke lineStroke = SymbolFactory.Instance.ConstructStroke(ColorFactory.Instance.BlackRGB, 1, SimpleLineStyle.DashDotDot);
        LayoutElementFactory.Instance.CreatePolygonGraphicElement(layout, env, SymbolFactory.Instance.ConstructPolygonSymbol(ColorFactory.Instance.RedRGB, SimpleFillStyle.Null, lineStroke));
        });
0 Kudos
BrennanJohnson
New Contributor II

Hi Jeff,

Your assumption is basically correct: I am allowing a user to set the font styling they desire, and at runtime, I am generating layout elements to display data with that selected styling. After generating these layout elements, I want to ensure that they are placed within a given area on the page and that they do not overlap.

I basically just want an Envelope-style object (although I wouldn't care if it was an actual Envelope) in which I can check the X and Y mins and maxes and get back values that correspond to the page coordinates. I would then be able to ignore any kind of rotation that the user decided to apply to text objects and simply look at the bounds of the generated element.

I found that paragraph text can work as an alternative, since you can directly specify the envelope that the text must be drawn within, but there are cases where it would be convenient to create point text instead. I appreciate the code snippets and will test those out in the meantime.

Thanks,

Brennan

0 Kudos
JeffBarrette
Esri Regular Contributor

We added a new method at ArcGIS Pro 2.6 called Element.GetBounds([bool rotated = false]) that will return either the unrotated or rotated envelope for an element.

      Layout layout = LayoutView.Active.Layout;
      await QueuedTask.Run(() =>
      {
        //Reference text element and get rotated and unrotated envelope
        TextElement txtElm = layout.FindElement("Text") as TextElement;
        Envelope bounds = txtElm.GetBounds(false);
        Envelope rot_bounds = txtElm.GetBounds(true);

        //Draw both envelopes on layout
        CIMStroke lineStroke1 = SymbolFactory.Instance.ConstructStroke(ColorFactory.Instance.BlackRGB, 1, SimpleLineStyle.DashDotDot);
        CIMStroke lineStroke2 = SymbolFactory.Instance.ConstructStroke(ColorFactory.Instance.RedRGB, 1, SimpleLineStyle.DashDotDot);
        LayoutElementFactory.Instance.CreatePolygonGraphicElement(layout, bounds, SymbolFactory.Instance.ConstructPolygonSymbol(ColorFactory.Instance.RedRGB, SimpleFillStyle.Null, lineStroke1));
        LayoutElementFactory.Instance.CreatePolygonGraphicElement(layout, rot_bounds, SymbolFactory.Instance.ConstructPolygonSymbol(ColorFactory.Instance.RedRGB, SimpleFillStyle.Null, lineStroke2));
      });

Jeff - Layout Team

BrennanJohnson
New Contributor II

Hi Jeff Barrette,

Thanks for the update. That method will be convenient to have.

After working more with text in layouts there are a few more utility methods/options that would be critical to what I'm trying to do, and which I'd expect would be extremely useful to users in general. In order of importance:

1) Option for paragraph text to both wrap to new lines and shrink font size automatically. Currently, the text automatically wraps to new lines, but when it reaches the bottom of its envelope, it gets cut off and shows an ellipsis symbol. It would be extremely nice to set some 'AutoShrink' property to 'true' and have the text automatically wrap to new lines and then shrink the font size until all of the text fits within the allotted envelope. Or, instead of having a boolean setting, a particularly useful way that this option could be implemented would be to have an property on the paragraph text like 'OverflowBehavior' that has some underlying enumeration of choices like 'ShowEllipsis', 'ShrinkFont', 'ExpandEnvelopeHorizontally', 'ExpandEnvelopeVertically'. In this way, a user could control exactly how the text should look without worrying about it being truncated.

2) It would be great to have the same GetBounds() method for paragraph text as you just added for point text, which would return the actual bounds of the drawn text. For instance, I may create a 6" X 6" envelope to draw my paragraph text in, but the text itself ends up being drawn in a 6" X 1.1" space, the GetBounds() method would return the 6" X 1.1" envelope. Currently, if you check the bounds on a paragraph text object, it simply gives you back the original envelope...you have no way to tell how much space is actually occupied by the text. In a situation like mine, where I am trying to arrange multiple text elements with dynamically generated text in a layout such that it takes up as little space as possible, it would be tremendously helpful to know the bounds of the drawn text on the screen.

3) Property on paragraph text that says whether or not the text overflowed its envelope. There is apparently no way to tell whether or not the text overflowed other than visually looking at the layout, where the tiny ellipsis indicator is shown. In my case, I am dynamically creating text elements from code, and want to prevent the ellipsis from ever appearing. Arc Pro obviously has access to this property, since it is showing the ellipsis in response, but there is apparently no way to access it from the API.

The underlying problem that all three of these issues address is this: I have the ability to draw paragraph text, but there is no way to ensure programmatically that the text fits within the specified envelope. I also have no way to check how much space the text is actually taking up, and therefore cannot optimize the placement of the text within the layout.

I really appreciate your continued efforts to improve the API and hope you consider adding at least one the suggestions above. 

Thanks again,

Brennan

0 Kudos