Ticket #180 (new enhancement)

Opened 20 months ago

Last modified 6 months ago

Parameter for line symbolizer to offset line to one side

Reported by: numenor Owned by: springmeyer
Priority: normal Milestone: 0.8.0
Component: Core Library Version: SVN Trunk
Severity: Normal Keywords: line symbolizer, shift, offset
Cc: steve8, migurski, paul, Skywave, ivansanchez, damiano.albani@… Patch Needs Improvement: no
Needs Docmentation: no Has Patch?: yes
Design Decision Needed: no

Description

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).

Attachments

springmeyer_europa_line_offsets2.patch (15.5 kB) - added by springmeyer 13 months ago.
Implementation of line offsets to allow for cartographic display of parallel/bundled lines based on one line feature
offsets_hollow.png (33.6 kB) - added by springmeyer 13 months ago.
Two offsets without showing original (non-displaced) line - parallel effect
offsets_multi.png (59.3 kB) - added by springmeyer 13 months ago.
Multiple, bundled line offsets of varying colors, width, and using dash_arrays
offsets_rainbow.png (73.6 kB) - added by springmeyer 13 months ago.
1 pixel displacement, 2 pixel wide lines in color ramp approximating cross-line gradient
offsets_directions.png (39.0 kB) - added by springmeyer 13 months ago.
blue is a negative -3 offset, and red is a postive +3 offset
offset-0.7.0.patch (12.0 kB) - added by dfaubion 7 months ago.
Patch for 0.7.0 release code. Blows up on sharp edges though.
sharp_spike_fix.patch (8.2 kB) - added by dfaubion 7 months ago.
Fixes the sharps spike for acute inner angles, but the continuity issues from line to line remain unchanged. Any ideas?
mapnik0.7.1-offsets.patch (13.8 kB) - added by mattmakesmaps 3 months ago.
patch for mapnik 0.7.1 created with help from dane springmeyer

Change History

  Changed 20 months ago by 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?.

  Changed 17 months ago by springmeyer

  • milestone changed from 0.6.0 to 0.7.0

pushing to 0.7.0 since it is an enhancement

  Changed 17 months ago by ivansanchez

Isn't this the same as #71?

  Changed 17 months ago by Ldp

A text-near-line hack? <TextSymbolizer .... size="10" placement="line" dy="10"/> works for me.

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

follow-up: ↓ 8   Changed 17 months ago by ivansanchez

  • keywords shift, offset added; shift removed

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).

  Changed 13 months ago by springmeyer

  • cc steve8, migurski, paul, Skywave added

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:


http://trac.mapnik.org/raw-attachment/ticket/71/offset.PNG

in reply to: ↑ 5   Changed 13 months ago by springmeyer

  • cc ivansanchez added

Replying to 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.

  Changed 13 months ago by springmeyer

  • owner changed from artem to springmeyer

  Changed 13 months ago by 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! :)

  Changed 13 months ago by 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

<CssParameter? name="stroke-offset">10</CssParameter>
<CssParameter? name="stroke-offset-symmetric">yes</CssParameter>

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

  Changed 13 months ago by springmeyer

  • has_patch set

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

In XML:

<CssParameter name="stroke-offset">[positive or negative float]</CssParameter>

In 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 '<CssParameter?? name="stroke-offset-symmetric">yes</CssParameter>'
  • 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

Changed 13 months ago by springmeyer

Implementation of line offsets to allow for cartographic display of parallel/bundled lines based on one line feature

Changed 13 months ago by springmeyer

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

Changed 13 months ago by springmeyer

Multiple, bundled line offsets of varying colors, width, and using dash_arrays

Changed 13 months ago by springmeyer

1 pixel displacement, 2 pixel wide lines in color ramp approximating cross-line gradient

  Changed 13 months ago by springmeyer

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


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

Multiple, bundled line offsets of varying colors, width, and using dash_arrays
Multiple, bundled line offsets of varying colors, width, and using dash_arrays

1 pixel displacement, 2 pixel wide lines in color ramp approximating cross-line gradient
1 pixel displacement, 2 pixel wide lines in color ramp approximating cross-line gradient

Changed 13 months ago by springmeyer

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

  Changed 13 months ago by springmeyer

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


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

<?xml version="1.0" encoding="utf-8"?>
<Map srs="+proj=merc +lon_0=0 +k=1 +x_0=0 +y_0=0 +a=6378137 +b=6378137 +units=m +no_defs " bgcolor="white">
    <Style name="1">
        <Rule>
            <LineSymbolizer>
                <CssParameter name="stroke">steelblue</CssParameter>
                <CssParameter name="stroke-offset">-3</CssParameter>
                <CssParameter name="stroke-width">1</CssParameter>
                <CssParameter name="stroke-linecap">round</CssParameter>
            </LineSymbolizer>
            <LineSymbolizer>
                <CssParameter name="stroke">red</CssParameter>
                <CssParameter name="stroke-offset">3</CssParameter>
                <CssParameter name="stroke-width">1</CssParameter>
                <CssParameter name="stroke-linecap">round</CssParameter>
            </LineSymbolizer>
            <MarkersSymbolizer />
        </Rule>
    </Style>
    <Layer name="layer" srs="+proj=merc +lon_0=0 +k=1 +x_0=0 +y_0=0 +a=6378137 +b=6378137 +units=m +no_defs ">
        <StyleName>1</StyleName>
        <Datasource>
            <Parameter name="file">../shp/polylines.shp</Parameter>
            <Parameter name="type">shape</Parameter>
        </Datasource>
    </Layer>
</Map>

  Changed 13 months ago by migurski

YES! yes, yes, yes. Awesome.

  Changed 13 months ago by springmeyer

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

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()

Changed 7 months ago by dfaubion

Patch for 0.7.0 release code. Blows up on sharp edges though.

Changed 7 months ago by dfaubion

Fixes the sharps spike for acute inner angles, but the continuity issues from line to line remain unchanged. Any ideas?

  Changed 7 months ago by snotling

  • cc damiano.albani@… added

  Changed 6 months ago by 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.

Changed 3 months ago by mattmakesmaps

patch for mapnik 0.7.1 created with help from dane springmeyer

Note: See TracTickets for help on using tickets.