简体   繁体   中英

SVG find rotation angle of a path

I've a problem with my SVG map. I use jVectorMap to create a custom map and I need to write the name of every field in the center of the field. The example is: JSFiddle Example (zoom in the right side to see the text)

I can find the center of every field with this function:

jvm.Map.prototype.getRegionCentroid = function(region){

    if(typeof region == "string")
        region = this.regions[region.toUpperCase()];

    var bbox = region.element.shape.getBBox(),
    xcoord = (bbox.x + bbox.width/2),
    ycoord = (bbox.y + bbox.height/2);

    return [xcoord, ycoord];
};

but my problem is that I want to rotate the text for align it with the top line of the relative field. I've tried with getCTM() function but it give me always the same values for every field.

How can I find the right rotation angle of every field?

Thank you to all!

Querying getCTM() won't help. All that gives you is a transformation matrix for the shape's coordinate system (which, as you discovered, is the same for every shape). To get a shape's vertex coordinates, you'll have to examine the contents of region.element.shape.pathSegList .

This can get messy. Although a lot of the shapes are drawn using simple "move-to" and "line-to" commands with absolute coordinates, some use relative coordinates and other types of line. I noticed at least one cubic curve. It might be worth looking for an SVG vertex manipulation library to make life easier.

But in general terms, what you need to do is fetch the list of coordinates for each shape (converting relative coordinates to absolute where necessary), and find the segment with the longest length. Be aware that this may be the segment between the two end points of the line. You can easily find the orientation of this segment from Math.atan2(y_end-y_start,x_end-x_start) .

When rotating text, make life easy for yourself by using a <g> element with a transform=translate() attribute to move the coordinate origin to where the text needs to be. Then the text won't shoot off into the distance when you add a transform=rotate() attribute to it. Also, use text-anchor="middle" and dominant-baseline="middle" to centre the text where you want it.

Your code should end up looking something like this:

var svg = document.getElementsByTagName('g')[0]; //Get svg element
var shape_angle = get_orientation_of_longest_segment(svg.pathSegList); //Write this function
var newGroup = document.createElementNS("http://www.w3.org/2000/svg","g");
var newText = document.createElementNS("http://www.w3.org/2000/svg","text");
newGroup.setAttribute("transform", "translate("+coords[0]+","+coords[1]+")"); 
newText.setAttribute("font-size","4");
newText.setAttribute("text-anchor","middle");
newText.setAttribute("dominant-baseline","middle");
newText.setAttribute("transform","rotate("+shape_angle+")");
newText.setAttribute('font-family', 'MyriadPro-It');
var textNode = document.createTextNode("C1902");
newText.appendChild(textNode);
newGroup.appendChild(newText);
svg.appendChild(newGroup);

Looks like squeamish ossifrage has beaten me to this one, and what they've said would be exactly my approach too...

Solution

Essentially find the longest line segment in each region's path and then orient your text to align with that line segment whilst trying to ensure that the text doesn't end up upside-down(!)

在此处输入图片说明

Example

Here's a sample jsfiddle

In the $(document).ready() function of the fiddle I'm adding labels to all the regions but you will note that some of the regions have centroids that aren't within the area or non-straight edges that cause problems - Modifying your map slightly might be the easiest fix.

Explanation

Here are the 3 functions I've written to demonstrate the principles:

  • addOrientatedLabel(regionName) - adds a label to the named region of the map.
  • getAngleInDegreesFromRegion(regionName) - gets the angle of the longest edge of the region
  • getLengthSquared(startPt,endPt) - gets length squared of line seg (more efficient than getting length).

addOrientatedLabel() places the label at the centroid using a translate transform and rotates the text to the same angle as the longest line segment in the region. In SVG transforms are resolved right to left so:

transform="translate(x,y) rotate(45)"

is interpreted as rotate first, then translate. This ordering is important!

It also uses text-anchor="middle" and dominant-baseline="middle" as explained by squeamish ossifrage . Failing to do this will cause the text to be misaligned within its region.

getAngleInDegreesFromRegion() is where all the work is done. It gets the SVG path of the region with a selector, then loops through every point in the path. Whenever a point is found that is part of a line segment (rather than a Move-To or other instruction) it calculates the squared length of the line segment. If the squared length of the line segment is the longest so far it stores its details. I use squared length because that saves performing a square root operation (its only used for comparison purposes, so squared length is fine).

Note that I initialise the longestLine data to a horizontal one so that if the region has no line segments at all you'll at least get horizontal text.

Once we have the longest line, I calculate its angle relative to the x axis with Math.atan2 , and convert it from radians to degrees for SVG with (angle / Math.PI) * 180 . The final trick is to identify if the angle will rotate the text upside down, and if so, to rotate another 180 degrees.

Note

I've not used SVG before so my SVG code might not be optimal, but it's tested and it works on all regions that consist mostly of straight line segments - You will need to add error checking for a production application of course!

Code

function addOrientatedLabel(regionName) {

    var angleInDegrees = getAngleInDegreesFromRegion(regionName);

    var map = $('#world-map').vectorMap('get', 'mapObject');
    var coords = map.getRegionCentroid(regionName);

    var svg = document.getElementsByTagName('g')[0]; //Get svg element
    var newText = document.createElementNS("http://www.w3.org/2000/svg","text");
    newText.setAttribute("font-size","4");
    newText.setAttribute("text-anchor","middle");
    newText.setAttribute("dominant-baseline","middle");
    newText.setAttribute('font-family', 'MyriadPro-It');
    newText.setAttribute('transform', 'translate(' + coords[0] + ',' + coords[1] + ') rotate(' + angleInDegrees + ')');
    var textNode = document.createTextNode(regionName);
    newText.appendChild(textNode);

    svg.appendChild(newText);    
}

Here's my method to find the longest line segment in a given map region path:

function getAngleInDegreesFromRegion(regionName) {

    var svgPath = document.getElementById(regionName);

    /* longest edge will default to a horizontal line */
    /* (in case the shape is degenerate): */
    var longestLine = { startPt: {x:0, y:0}, endPt: {x:100,y:0}, lengthSquared : 0 };

    /* loop through all the points looking for the longest line segment: */
    for (var i = 0 ; i < svgPath.pathSegList.numberOfItems-1; i++) {
        var pt0 = svgPath.pathSegList.getItem(i);
        var pt1 = svgPath.pathSegList.getItem(i+1);
        if (pt1.pathSegType == SVGPathSeg.PATHSEG_LINETO_ABS) {
            var lengthSquared = getLengthSquared(pt0, pt1);
            if( lengthSquared > longestLine.lengthSquared ) {
                longestLine = { startPt:pt0, endPt:pt1, lengthSquared:lengthSquared};                 
            }            
        }/* end if dealing with line segment */                   
    }/* end loop through all pts in svg path */    

    /* determine angle of longest line segement relative to x axis */
    var dY = longestLine.startPt.y - longestLine.endPt.y;
    var dX = longestLine.startPt.x - longestLine.endPt.x;
    var angleInDegrees = ( Math.atan2(dY,dX) / Math.PI * 180.0);

    /* if text would be upside down, rotate through 180 degrees: */
    if( (angleInDegrees > 90 && angleInDegrees < 270) || (angleInDegrees < -90 && angleInDegrees > -270)) {
        angleInDegrees += 180;
        angleInDegrees %= 360;
    }    

    return angleInDegrees; 
}

Note that my getAngleInDegreesFromRegion() method will only consider the longest straight line in a path if it is created with the PATHSEG_LINETO_ABS SVG command... You'll need more functionality to handle regions which don't consist of straight lines. You could approximate by treating curves as straight lines with:

if (pt1.pathSegType != SVGPathSeg.PATHSEG_MOVETO_ABS ) 

But there will be some corner cases, so modifying your map data might be the easiest approach.

And finally, here's the obligatory squared distance method for completeness:

function getLengthSquared(startPt, endPt ) {
    return ((startPt.x - endPt.x) * (startPt.x - endPt.x)) + ((startPt.y - endPt.y) * (startPt.y - endPt.y));
}

Hope that is clear enough to help get you started.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM