Skip to content
This repository

Parameter for line symbolizer to offset line to one side #180

Closed
artemp opened this Issue October 11, 2011 · 31 comments

2 participants

Artem Pavlenko Dane Springmeyer
Artem Pavlenko
Owner

An additional parameter for the line symbolizer, which would allow to shift a line asymmetrically to one side is currently missing.

It would allow some visualizations currently not possible: e.g. one side of a road could be painted in a color indicating a cycle way on that side, or several hiking routes with different colors could be rendered side by side instead of one route hiding the other.

The shift should be specifiable in pixels (to be able to make it consistent with line widths etc.), and maybe alternatively in map units (to paint a second line with a known constant distance to another one).

The difference to ticket http://trac.mapnik.org/ticket/51 would be, that the line would not be a border, but could have a larger distance or could overlap a main line, there could even be no main (unshifted) line, and it would allow asymmetric shifts (to only one side instead of always on both sides).

Artem Pavlenko
Owner

[migurski] It would be awesome if text could be offset in this way too. I'm thinking about Andy Allan's hacks to get text-near-line working in OpenCycleMap.

Artem Pavlenko
Owner

[springmeyer] pushing to 0.7.0 since it is an enhancement

Artem Pavlenko
Owner

[ivansanchez] Isn't this the same as #71?

Artem Pavlenko
Owner

[Ldp] A text-near-line hack?
works for me.

The other part of the request, offset placement, is interesting, but as said, I think largely the same as #71.

Artem Pavlenko
Owner

[ivansanchez] No, this is not about text-near-line, it's about line-near-line. I think a LinePatternSymbolizer, with a 1-pixel-wide transparent image could serve as a hack for this, but offsetting the actual line vectors would be the desireable thing to have. In fact, the offset parameter would be ideally be applied to LinePatternSymbolizer too.

And, as #71 says, you don't want to shift, you want to offset. And, by implementing offsets, you effectively solve #51 (just add two offset LineSymbolizers, one for each side of the line).

Artem Pavlenko
Owner

[springmeyer] I'm going to mark #71 a duplicate of this ticket and keep this one as it includes more details.

There is a notion of re-joining lines from #71 that will need to be fleshed out more, as well as this nice graphic which should remain:

[[BR]]
[[Image(http://trac.mapnik.org/raw-attachment/ticket/71/offset.PNG)]]

Artem Pavlenko
Owner

[springmeyer] Replying to [comment:5 ivansanchez]:

No, this is not about text-near-line, it's about line-near-line. I think a LinePatternSymbolizer, with a 1-pixel-wide transparent image could serve as a hack for this, but offsetting the actual line vectors would be the desireable thing to have. In fact, the offset parameter would be ideally be applied to LinePatternSymbolizer too.

And, as #71 says, you don't want to shift, you want to offset. And, by implementing offsets, you effectively solve #51 (just add two offset LineSymbolizers, one for each side of the line).

Good points ivansanchez. I've marked those other tickets as duplicates of this one accordingly. #350 will remain as a proposal for a longer term, more sophisticated solution.

Artem Pavlenko
Owner

[Ldp] Part of the needs of #335 could be solved with this. Lay down two offset strokes, and you have an unpainted core. It wouldn't work too cleanly with self-intersecting lines, but let's attack this one issue at a time! :)

Artem Pavlenko
Owner

[Ldp] AIUI, in the current prototype, you need 2 LineSymbolizers to lay down a casing on both sides of a line. A possible shortcut would be to allow something like

10[[BR]]
yes

And internally it could create two symbolizers for that, one with +10 and another with -10 offset.

Artem Pavlenko
Owner

[springmeyer] Patch attached implements line offsets for the LineSymbolizer by adding a 'stroke-offset' parameter.

In XML:

{{{
#!xml
[positive or negative float]
}}}

In Python:
{{{
#!python

from mapnik import LineSymbolizer
l = LineSymbolizer()
l.stroke.offset = -10
}}}

Also includes tests that can be run with the existing nose suite:

python tests/run_tests.py

or with nik2img.py:

nik2img.py tests/data/good_maps/polyline_offsets_map.xml offsets.png

ToDo Items include:

  • Thinking through ways to abstract the interface slightly like Ldp's idea above of 'yes'
  • Performance testing to make sure that typedef coord_transform4<CoordTransform,geometry2d> path_type; does not slow down non-offset lines
    • I likely need to conditionally typedef the path_type (depending on if an offset is requested)
  • More testing
  • Scoping of feasibility of addition of functionality to the LinePatternSymbolizer

Thanks to Marcin and #332 for inspiration and ad example for solving this problem within ctrans.hpp

Artem Pavlenko
Owner

[springmeyer] Two offsets without showing original (non-displaced) line - parallel effect

[[BR]]
[[Image(offsets_hollow.png)]]

Multiple, bundled line offsets of varying colors, width, and using dash_arrays
[[BR]]
[[Image(offsets_multi.png)]]

1 pixel displacement, 2 pixel wide lines in color ramp approximating cross-line gradient
[[BR]]
[[Image(offsets_rainbow.png)]]

Artem Pavlenko
Owner

[springmeyer] blue is a negative -3 offset, and red is a postive +3 offset

[[BR]]
[[Image(offsets_directions.png)]]

{{{
#!xml
<?xml version="1.0" encoding="utf-8"?>

<br> <Rule><br> <LineSymbolizer><br> <CssParameter name="stroke">steelblue</CssParameter><br> <CssParameter name="stroke-offset">-3</CssParameter><br> <CssParameter name="stroke-width">1</CssParameter><br> <CssParameter name="stroke-linecap">round</CssParameter><br> </LineSymbolizer><br> <LineSymbolizer><br> <CssParameter name="stroke">red</CssParameter><br> <CssParameter name="stroke-offset">3</CssParameter><br> <CssParameter name="stroke-width">1</CssParameter><br> <CssParameter name="stroke-linecap">round</CssParameter><br> </LineSymbolizer><br> <MarkersSymbolizer /><br> </Rule><br>

1

../shp/polylines.shp
shape



}}}

Artem Pavlenko
Owner

[migurski] YES! yes, yes, yes. Awesome.

Artem Pavlenko
Owner

[springmeyer] the single sided buffer has now landed in geos and been exposed in postgis trunk: http://trac.osgeo.org/postgis/ticket/413

So, this offers a means to test this patch against similar functionality that works in geographic space to offset lines.

Thanks dfaubion for the new work. I've not had a chance to take a look yet, but will after things wrap up with the next release (0.7.1). Hopefully we can get this patch into trunk soon for more testing. I think one hold up is that we need to give though to how to support things such as offsets along with other methods that modify the geometry on the fly like the smoothing work in #332. It make not be possible to support them both on the same symbolizer, but ideally we could.

Artem Pavlenko
Owner

[springmeyer] The original implementation in python. Useful for testing new features:

{{{
#!python
import os
import math
import cairo

output = "/tmp/output2.svg"

width, height = 600, 400

class Coord:
def init(self,x,y):
self.x = float(x)
self.y = float(y)

class Segment:
def init(self,coord_a,coord_b):
self.ca = coord_a
self.cb = coord_b

@property
def angle(self):
    dy = self.cb.y - self.ca.y
    dx = self.cb.x - self.ca.x
    return math.atan2(dy,dx)

class Joint:
def init(self,segment_a,segment_b):
self.sa = segment_a
self.sb = segment_b

def displace_by(self,coord,offset):
    angle = self.sa.angle
    sin_a = offset * math.sin(angle + math.pi/2)
    cos_a = offset * math.cos(angle + math.pi/2)

    h = math.tan((self.sb.angle - self.sa.angle)/2.0)

    cx = coord.x + cos_a - h * sin_a
    cy = coord.y + sin_a + h * cos_a

    return Coord(cx,cy)

def displace(c,angle,offset):
dx = offset * math.cos(angle + math.pi/2)
dy = offset * math.sin(angle + math.pi/2)
return Coord(c.x+dx,c.y+ dy)

def draw_offset_line(ctx, coords, offset, color=(0,0,0)):
ctx.set_source_rgba(*color)
segment_a = None
segment_b = None
last_coord = None

for idx,i in enumerate(coords):
  if idx == 0:
      segment = Segment(i,coords[idx+1])
      c1 = displace(i,segment.angle,offset)
      last_coord = i
  elif idx == len(coords)-1: # last coord
      segment = Segment(coords[idx-1],i)
      c1 = displace(i,segment.angle,offset)
  else: # we have a last_coord
      segment_a = Segment(last_coord,i)
      segment_b = Segment(i,coords[idx+1])
      joint = Joint(segment_a,segment_b)
      c1 = joint.displace_by(i,offset)
      last_coord = i

  if idx == 0:
      ctx.move_to(c1.x, c1.y)
  else:
      ctx.line_to(c1.x, c1.y)

ctx.stroke()

def draw_line(ctx, coords, color=(0,0,0)):
ctx.set_source_rgba(*color)
start = coords[0]
ctx.move_to(start.x, start.y)
for i in coords:
ctx.line_to(i.x, i.y)
ctx.stroke()

def main():

vertices = [[10,10],[23,45],[67,90],[90,67],[90,34],[200,150],[150,200],[550,350],[34,375]]
coords = [Coord(a,b) for a,b in vertices]

black = (0,0,0)
red = (255,0,0)
green = (0,255,0)
blue = (0,0,255)

surface = cairo.SVGSurface(output, width, height)
context = cairo.Context( surface )
context.set_source_rgb( .5, .5, .5)
context.rectangle( 0, 0, width, height)
context.fill()
context.set_line_width(10)
context.set_font_size(7)

# draw original line
draw_line(context,coords,color=black)

# draw line offset positively (right side)
draw_offset_line(context,coords,10,color=green)

# draw line offset negatively (left side)
draw_offset_line(context,coords,-12,color=red)

context.show_page()
surface.finish()

os.system('open %s' % output)

main()
}}}

Artem Pavlenko
Owner

[dpaleino] Is there any news on this ticket? :)

I'm planning to do some offset-drawing in the near future, and I'm scared by the way to do this with current Mapnik ;-)

Artem Pavlenko
Owner

[springmeyer] Hi David. It is on my list to get integrated into trunk, but the list is long :) You should be able to apply the 'mapnik0.7.1-offsets.patch' patch to mapnik 0.7.1 to try things out. It would be great to get your feedback on how the patch works.

Artem Pavlenko
Owner

[djakk] Hello, I've noticed 3 bugs in the case "angle_joint" :

{{{
case angle_joint:
dx_curr = cos(angle_a + M_PI/2);
dy_curr = sin(angle_a + M_PI/2);

                    sin_curve = dx_curr*dy_pre-dy_curr*dx_pre;
                    cos_curve = -dx_pre*dx_curr-dy_pre*dy_curr;

                    #ifdef MAPNIK_DEBUG
                        std::clog << "sin_curve value: " << sin_curve << "\n";
                    #endif
        angle_b = atan2((m_cur_y-m_next_y),(m_cur_x-m_next_x));
        if (-0.3 < sin_curve && sin_curve < 0.3) 
          {
            h = tan((angle_b - angle_a)/2.0); 
            *x = m_cur_x + dx_curr * offset_ - h * dy_curr * offset_; 
            *y = m_cur_y + dy_curr * offset_ + h * dx_curr * offset_; 
          } 
        else {
          base_shift = -1.0*(1.0+cos_curve)/sin_curve;
          *x = m_cur_x + (dx_curr - base_shift*dy_curr)*offset_;
          *y = m_cur_y + (dy_curr + base_shift*dx_curr)*offset_;
        }

                    // Save old shit
                    m_cur_x = m_next_x; 
                    m_cur_y = m_next_y; 
                    angle_a = angle_b;
        dx_pre = dx_curr;
        dy_pre = dy_curr;
                    m_pre_cmd = m_cur_cmd; 
                    m_cur_cmd = m_next_cmd; 
                    m_status = process; 
                    return m_pre_cmd; 

}}}

(sorry, I don't know how to create a patch …)

Artem Pavlenko
Owner

[springmeyer] Thanks for the report!

create a patch by checking out from svn and editing the file then doing:
{{{
svn diff include/mapnik/ctrans.hpp
}}}

Or just by grabbing an original and doing:
{{{
diff -rcs edited_file.hpp original_file.hpp
}}}

Artem Pavlenko
Owner

[djakk] I've managed to create a patch for mapnik 0.7.1 (it replaces the old one).

I've fixed those bugs :
[[Image(http://trac.mapnik.org/raw-attachment/ticket/180/bug-fixed-base-shift.png)]]
[[Image(http://trac.mapnik.org/raw-attachment/ticket/180/bug-fixed-update-angle-a.png)]]

Yet another bug is not solved : when a segment is too small compared to the offset, the offset line makes a loop
[[Image(http://trac.mapnik.org/raw-attachment/ticket/180/bug-TODO-segment-is-too-small.png)]]

Offset is not easy, look at this paper : http://cgcad.thss.tsinghua.edu.cn/~yongjh/papers/CiI2007V58N03P0240.pdf :O

Artem Pavlenko
Owner

[djakk] My previous fix brings a new bug … so here comes a new patch !
Actually I did not manage to understand the "sharp_spike_fix.patch" very well :-( so I've re-written it.
The sharp spike is not taken into account in the offset line.

This may be an other temporary offset patch, as the ultimate solution may come from an algorithm based on straight skeletons (http://en.wikipedia.org/wiki/Straight_skeleton)

Artem Pavlenko
Owner

[springmeyer] djakk, thanks for the further work. I agree, I never saw how the 'sharp_spike_fix.patch' improve things, but rather just introduced bugs. I will take a look at your patch sometime in the next several weeks.

Artem Pavlenko
Owner

[TobWen] djakk, thanks for your patch, but I'm not able to apply it to Mapnik 0.7.1, official release. I'm getting "hunk FAILED" several times.

Are you applying it on the content of mapnik-0.7.1.tar.bz2 from BerliOS or some build from SVN?

Artem Pavlenko
Owner

[TobWen] Replying to [comment:26 TobWen]:

I've fixed it... wrong patch parameters (I've used p1 instead of p0) :-/

Artem Pavlenko
Owner

[Petr Dlouhy] Hello,

I have improve the patch, so now it works also for LinePatternSymbolizer with attribute offset.

Artem Pavlenko
Owner

[Petr Dlouhy] xificurk: are you sure, you have attached all the changes? The patch seems incomplete.

Artem Pavlenko
Owner

[xificurk] Replying to [comment:30 Petr Dlouhy]:

xificurk: are you sure, you have attached all the changes? The patch seems incomplete.

Seems like some space monkeys got to the previous upload, thanks for notifying me. I've corrected the problem.

Dane Springmeyer
Owner

so, there have been a few patch updates here since I last followed this ticket. Thanks for all the contributions and effort on this and sorry for abandoning (temporarily). So, I failed to post some work I did getting this working with mapnik 2.x. So, I am going to push that partial work now into trunk - the complex bit in ctrans.hpp - so that all other folks that have hacked at this can start working on the same code.

But I'm not planning on enabling this feature until I have time to properly ensure this ctrans class can be chained with others. I will create another ticket for tracking (and explaining this).

Dane Springmeyer springmeyer closed this issue from a commit October 21, 2011
Dane Springmeyer add a new, experimental coord_transform impl to support offsetting li…
…ne verticies - closes #180 - next task of exposing functionality refs #927
cad0c60
Dane Springmeyer springmeyer closed this in cad0c60 October 21, 2011
Dane Springmeyer
Owner

further work needed, which will be tracked starting with #927

Konstantin Käfer kkaefer referenced this issue from a commit in kkaefer/mapnik October 21, 2011
Dane Springmeyer add a new, experimental coord_transform impl to support offsetting li…
…ne verticies - closes #180 - next task of exposing functionality refs #927
c92e7ab
Dane Springmeyer
Owner

improved in #1269.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Something went wrong with that request. Please try again.