[R-sig-Geo] Better label placement for polygons

Karl Ove Hufthammer karl at huftis.org
Sat Aug 11 17:30:52 CEST 2012


Karl Ove Hufthammer skreiv:

> I have had some problems with bad label placements for SpatialPolygons,
> so I wrote a small function for improving it. Hope it will be of use to
> someone on this list. Here’s an example of its output:
> 
>   http://huftis.org/kritikk/polygon-labels.png

I now have an alternative, which takes into account the width and height 
of the label. It’s based on a brute-force search of candidate positions, 
and selects the position where the maximum distance between the label 
rectangle and the polygon edge is the greatest. Here’s an example of the 
function applied to a long and a tall string:

  http://huftis.org/kritikk/polygon-labels-rect.png

And here’s the source code:

-----
library(rgdal)
library(rgeos)

calc.labpt.strings=function(pol, label, gridpoints=2000) {
  # Fetch the label size
  wd=strwidth(label)
  ht=strheight(label)
  
  # Sample a regular grid of points inside the polygon
  co=coordinates(spsample(pol, n=gridpoints, type="regular"))
  
  # Create a rectangular polygon with a given size and at a given position
  makerect=function(x, y, wd, ht, rectID)
      Polygons(list(Polygon(cbind(c(x,x+wd,x+wd,x,x), c(y,y,y+ht,y+ht,y)))), rectID)
  
  # Create a candidate label rectangle for each grid point
  # (Note that ‘co’ may have fewer than ‘gridsize’ rows.)
  rects=SpatialPolygons(sapply(seq_len(nrow(co)), function(i) makerect(co[i,1], co[i,2], wd, ht, i)),
                        proj4string=CRS(proj4string(pol)))
  
  # Only keep the rectangles that are fully inside the polygon
  inside=apply(gContains(pol, rects, byid=TRUE), 1, any)
  if( all(!inside)) # Abort if no candidate label positions can fit the label
    stop("Could not fit label inside polygon (with the current number of gridpoints")
  rects=rects[inside,]
  
  # Convert the polygon to lines, and then measure the distance
  # from each label rectangle to the nearest line, keeping the
  # one with the largest distance.
  pol.l=as(pol, "SpatialLines")
  ind=which.max(apply(gDistance(pol.l, rects, byid=TRUE), 1, min))
  labelpos=apply(bbox(rects[ind,]), 1, mean)
  labelpos
}
-----


Here’s an example of how to use the function. Note that we have 
to draw the polygon *before* using the function, as the function
uses coordinate information from the graphical device.

The function uses 2000 candidate grid points by default, and is
still quite fast, but one can usually get by with fewer points
(even as few as 200 usually gets good results).

-----
# Fetch an example map
library(rworldmap)
kart=getMap(projection="equalArea")
xy.sp=kart["BRA",]

# Two examples
par(mfrow=c(1,2))

# A long (wide) string
plot(xy.sp, col="khaki")
label="A very long text string"
xy=calc.labpt.strings(xy.sp, label)
text(xy[1], xy[2], label)

# A tall string
plot(xy.sp, col="khaki")
label="N\na\nr\nr\no\nw"
xy=calc.labpt.strings(xy.sp, label)
text(xy[1], xy[2], label)
-----


Two final remarks:

The function seems to give excellent results on most *natural* 
maps, but for very regular polygons, my previous label 
placement function is better. This is because the optimality
criterion only looks at the maximum distance, and it doesn’t
handle ‘ties’ in a smart way either (it uses the first candidate 
point which satisfies the criterion).

The function will only work properly on projected polygons
(or polygons plotted with ‘asp=1’), and will warn if the
SpatialPolygons object is not projected. One good alternative 
for longlat data is to use the equirectangular projection, as
described in my previous post:

  http://article.gmane.org/gmane.comp.lang.r.geo/15170

I’d appreciate any comments and suggestions for improvements.

-- 
Karl Ove Hufthammer
E-mail: karl at huftis.org
Jabber: huftis at jabber.no



More information about the R-sig-Geo mailing list