Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How to render streets with a patters that behaves similar to “polygon-pattern”? #3977

Closed
sommerluk opened this issue Aug 25, 2018 · 14 comments

Comments

@sommerluk
Copy link

Hello.

At https://github.com/gravitystorm/openstreetmap-carto we are trying to render unpaved roads different from paved ones.

Normally, I would avoid to ask this type of questions here on the issue tracker, but I started to write the code for openstreetmap-carto in May 2017, since than the solution is in continuous development, we have tested a lot of possible solutions, but I still struggle to have a well-performing solution. That’s why now, after one year, finally I ask here.

What is the desired rendering?

Paved roads

We want to render paved roads still like they do currently in openstreetmap-carto: with a plain colour.

Unpaved roads

Unpaved roads should however be rendered from now on with a fine, irregular-looking pattern. At this image you can see the rendering we want to get. The irregular pattern fills exactly the space that would have been occupied for a paved road. Neither at crossings between two unpaved roads nor at crossings between a paved and an unpaved road there is any visual artefact.

Further requirements

We are using round line caps for roads to avoid visual artefacts at places where OSM ways are split.

line-pattern?

The natural choise would be Mapnik’s line-pattern feature. Mapnik renders this pattern following the geometry of the OSM way, blending it at angles within the OSM way. See here for a minimalistic example. But I do not get the desired rendering because line-pattern seems to have no support for round line caps, so this leads to ugly rendering artefacts on every single angle in the line, and especially where OSM ways end. (Furthermore, we need many pattern files: one for each combination of road colour and width. But the main problem with this solution are the rendering artefacts.)

dash-array?

We could use various dash-arrays to try to get a similar visual effect. But also here, we have rendering artefacts.

polygon-pattern on line geometries?

A polygon-pattern would be the perfect solution. It is not blended at angles. It can even be aligned globally, so that on crossings of two unpaved roads, the pattern makes a smooth connection. It would also play well together with highway area rendering, which are polygons. But unfortunately it is not possible to use polygon-pattern with line geometries in Mapnik. (This would be a great feature! Something like applying an SVG pattern to a line geometry just like you would apply a colour – the pattern would only be visible where the line is drawn, and the pattern itself could ideally be aligned globally.)

Our workaround…

What is the principle of the workaround?

As there is no build-in support for polygon-pattern on line geometries, we have developed a workaround. In short:

  • We apply line-comp-op: dst-out; to the line geometries of the unpaved roads to cut holes in the rendering canvas.
  • In the SQL query, our data source, we create a fake geometry which is a global bounding box for the whole world. This fake geometry, we render once for each different road colour as polygon-pattern using dst-over behind the existing canvas.

There is a short, but complete documentation available.

What’s the problem with the workaround?

We have about 10 different road colours. Reading the Mapnik documentation about comp-op, we would expect that feature-level comp-op would work on a per-feature base. Quote from the documentation:

Feature-level compositing, during rendering, means that for every geometry processed the […] operation will be used to blend the rendered pixels of the polygon against the destination pixels (all data previously rendered on the canvas whether from previous layers or just another polygon from the same style).

But apparently this does not happen. What we observe is:

  1. We have yet various things like landcovers rendered. Let’s call this canvas A.
  2. Render white unpaved roads: Cut a hole in the existing image by using comp-op. Now we have canvas A with some holes.
  3. Render the white pattern behind: It will be only visible where we have the holes in canvas A that have been done in the previous step. Now we have still canvas A with some holes. And we have canvas B with the white pattern on the global bounding box. Canvas B is behind canvas A.
  4. Render white paved roads. They will be rendered in canvas A.
  5. Render yellow (higher road class!) unpaved roads: Cut a hole in the existing image by using comp-op. This cuts more holes in canvas A (only!), but does not touch canvas B.
  6. Render the yellow pattern behind: This will create canvas C, which will be behind canvas B. It will therefore never be visible.

That’s bad for our use case. So I have worked around this problem: In our MSS code (we are using CartoCSS) I’ve created attachments. This way, the rendering rules for each road colour end up in an own attachment (all of them within the same layer). Also the fake-global-bounding-box gets its own attachments: one for each road colour, in the MSS code always directly after the rendering rules for this road colour. This leads to a perfect rendering without any artefacts. So the pull request implementing this for openstreetmap-carto had been merged.

Unfortunately, it turns out that just using many attachments makes the rendering process slower – by factor 2 or 3 measured for the whole style! Given that the whole style has also many other layers than the roads, the attachments itself will make a much greater slow-down for its own layer only, maybe factor 50? (That’s even true when these attachments do not actually get rendered because there is no data available for them in the data source.) The pull request has therefore been reverted.

My questions

  1. Are we doing something wrong? Maybe overlooking at all a better approach to get the same rendering?
  2. Are we doing something wrong with the comp-op? Can we get it working really on a per-feature base as described in the Mapnik documentation, and so avoid using attachments?
  3. If there is no other approach, would it be possible to tune the current approach somehow, to avoid an extreme slow-down because of the attachments?

If we could some help or some ideas here, that would be great – I would highly appreciate!

@talaj
Copy link
Member

talaj commented Aug 27, 2018

The reason why attachments are slow is most likely following. Attachments are converted to styles, for example:

<Layer name="roads-fill">
  <StyleName><![CDATA[roads-fill-secondary-fill]]></StyleName>
  <StyleName><![CDATA[roads-fill-secondary-fill-pattern]]></StyleName>
  <StyleName><![CDATA[roads-fill-primary-fill]]></StyleName>
  <StyleName><![CDATA[roads-fill-primary-fill-pattern]]></StyleName>

  <Datasource>
    ...
  <Datasource>
</Layer>

Mapnik is querying repeatedly the same data from the datasource for every style. There is Layer option cache-features for caching data between styles, but this option is set to off by default as it can consume a lot of memory.

@talaj
Copy link
Member

talaj commented Aug 27, 2018

Are we doing something wrong with the comp-op? Can we get it working really on a per-feature base as described in the Mapnik documentation, and so avoid using attachments?

The current implementation with CartoCSS attachments is per-feature compositing already. I think there was some another problem. Namely rendering order, in which all dst-out holes were filled with one pattern with no holes left for another pattern. But this is just my hypothesis. I did not study XML version of that style yet.

@talaj
Copy link
Member

talaj commented Aug 27, 2018

Are we doing something wrong? Maybe overlooking at all a better approach to get the same rendering?

Ideal would be to have a symbolizer that can render such line patterns natively. It would not be so complicated to implement. Lines actually are polygons rendered with solid color. We know how to render polygons with a pattern.

@sommerluk
Copy link
Author

There is Layer option cache-features for caching data between styles, but this option is set to off by default as it can consume a lot of memory.

Thanks. I’ve tested this, and indeed with cache-features it’s as fast as without using layers. (But my local machine is not a representative environment for a production server.) How problematic is the memory issue that you mentioned? I didn’t detect any problems locally…

@talaj
Copy link
Member

talaj commented Sep 2, 2018

cache-features was turned off by default after #657.

I have never encountered memory issue with cache-features personally, but it can happen when querying large amount of data from a database, for example. It is good prevention to select data which are actually rendered, filter data rather in the database instead of in the style. This is good approach regardless of cache-features setting.

@sommerluk
Copy link
Author

@talaj Thanks.

@talaj
Copy link
Member

talaj commented Sep 22, 2018

An additional question: At openstreetmap-carto, we try to use the pattern rendering style not only for roads (line geometries), but we do the same also for turning circles (point geometries, that we render with a marker symbolizer as circles in the same colour as the road, and with varying diameters). Would the new PolygonPatternSymbolizer also accept point geometries, just rendering as circles with the diameter specified in stroke-width?

@sommerluk, I was investigating options how to render points as line caps given the information Cairo handles degenerate geometries that way. As you can read from the doc, there need to be a cairo_close_path() or cairo_line_to() call so the geometry needs to be polygon or polyline. It will not work with just point geometries.

What do you think about it? You will need to provide the point in one of these forms:

LINESTRING(x y, x y)
POLYGON((x y, x y))

The query would look like:

SELECT ST_MakeLine(geom, geom) FROM points

@sommerluk
Copy link
Author

@talaj

Would that mean it will work only for the Cairo backend, or also for the AGG backend?

I’ve tried to use ST_MakeLine(geom, geom) on the released 3.0.20 Mapnik with a LineSymbolizer and it didn’t work. The same applies for specially prepared data: The two points must have a certain distance at least for making the geometry rendered. Is the reason that the implementation in 3.0.20 is different from what you are planning currently? Or is the reason the AGG backend?

@talaj
Copy link
Member

talaj commented Sep 23, 2018

@sommerluk

Would that mean it will work only for the Cairo backend, or also for the AGG backend?

It means that AGG backend would be unified with Cairo backend regarding handling of degenerate geometries.

It is possible to rendered LINESTRING(x y, x y) with Cairo backend already.

@sommerluk
Copy link
Author

Sounds good!

(I’ve played around a little bit with this on line symbolizer and Cairo backend.)

At openstreetmap-carto we would than need support for ST_MakeLine(geom, geom) line with identical start and end points

  • on both, line (for paved turning circles) and line-pattern (for unpaved turning circles) symbolizer

and

  • on both, AGG and Cairo rendering backends.

This might maybe even give us the possibility to put turning circles within the same SQL querry as roads…

@talaj
Copy link
Member

talaj commented Oct 23, 2018

@sommerluk
After a delay, I just created a pull request with the line pattern improvements to v3.0.x branch. I should have managed to do it before Artem released v3.0.21. I was trying to modify the line-cap behavior on degenerate geometries, but with no success.

From my point of view, it cannot be naturally done to current line drawing code of AGG renderer. Needed changes are not balanced by marginal improvement in functionality.

My current opinion is that we should look for different solution.

@talaj
Copy link
Member

talaj commented Oct 24, 2018

Thinking about introducing PointPatternSymbolizer which would render pattern in a circle. This could be extended to arbitrary shape given by SVG file in the future.

@sommerluk
Copy link
Author

@talaj That sound great!

@sommerluk
Copy link
Author

This feature has been implemented native in #3980 for master, backported to v3.0.x. in #4004, documented in mapnik-reference which got a new npm release already.

Thanks to the hole Mapnik team and especially @talaj for implementing this!

Closing this issue now.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants