Skip to content
This repository has been archived by the owner on Jan 12, 2024. It is now read-only.

Support Format and Layout nodes in text symbolizer #347

Open
hollinger opened this issue May 21, 2014 · 9 comments
Open

Support Format and Layout nodes in text symbolizer #347

hollinger opened this issue May 21, 2014 · 9 comments
Milestone

Comments

@hollinger
Copy link

This is related to #238, but is a somewhat separate issue from list placements. I would like to see carto support for child formats and layouts within a text symbolizer, other than writing XML directly into the text-name field. I am interested in supporting Layout in particular, which is a new feature that I added in mapnik (See mapnik/mapnik#2138). I am thinking about implementing this on my fork of this project for now.

Has any thought been given to what the syntax for creating this structure would look like in carto?

It occurred to me that perhaps these could be created using instance names, similar to additional symbolizers, and then referenced in the name field in some way.

Simple example:

#label {
  text-face-name:@font_reg;
  text-name:"[name1]$boldtext$";
  boldtext/format-face-name:@font_bold;
  boldtext/format-name:"[name2]";
}

An xml format node would be created from the provided css attributes within the instance name and then substituted into the symbolizer content where referenced. Layout could work the same way, and the instance names could be used to override attributes of the individual formats and layouts. This is an initial idea, and could probably use some improvement.

I was wondering if anyone has been working on this at all or has a better idea about what the syntax should look like.

@springmeyer
Copy link

Has any thought been given to what the syntax for creating this structure would look like in carto?

Not that I am aware, so I'm you are planning on working on this.

Thanks for kicking off discussion of syntax ideas - certainly challenging to figure out how to model this in CSS. But finding a good syntax is critical. We should feel free to change the way Mapnik XML/load_map parsing works if needed.

As far as your idea - seems like a good start, but I think it would be ideal to have something different than the existing instance syntax to avoid confusion.

What about turning https://github.com/mapnik/mapnik/wiki/TextSymbolizer#layouts into:

#label {
  text-face-name:@font_reg;
  text-name: layout (
     dy: -5;
     text-name:"[name1]";
  ), layout (
     dy: 5;
     text-name:"[name2]";
  );
}

This would be more like the list of stops syntax of the raster-colorizer-stops (https://www.mapbox.com/tilemill/docs/guides/discrete-raster-data/#importing_and_styling_in_tilemill). The duplicated text-name is weird, maybe instead it could be text-layout: ( ... ), (....);

@springmeyer
Copy link

/cc @rcoup for help and critque

@rcoup
Copy link

rcoup commented May 22, 2014

Random dump of ideas...

CSS3 pseudo-elements is the first thing that kinda springs to mind...

<q>Some quotes</q>, he said, <q>are better than none</q>.
q::before { 
  content: "«";
  color: blue;
}
q::after { 
  content: "»";
  color: red;
}
«Some quotes», he said, «are better than none».

There's been some (abandoned) discussion about Nesting pseudo-elements in CSS3 which could provide some helpful ideas? Along those lines...

#label {
  text-face-name: @font_reg;
  text-name: "[name1]";
  text-size: 14;
  text::before {
    text-name: "First: ";
    text-size: 9;
    dy: -5;
  }
  text::after(1) {
    text-name: " Last: ";
    text-size: 9;
    dy: -5;
  }
  text::after(2) {
    text-name: "[name2]";
    text-face-name: @font_bold;
  }
}

Which is along the lines of your layout(...) idea (though yours doesn't add confusion about what :: means!). I guess text-before(n): ... or text-before: (and using ordering to dictate precedence) are all fairly similar.

Some of the text symbolizer properties can probably only apply to the "main" symbolizer? eg. placement.

How that maps easily back to mapnik's XML or internals I haven't thought through... though at an XML level it could map to something like (simplistically):

<TextSymbolizer face-name="DejaVu Sans" size="14">
  <TextSymbolizer size="9" dy="-5">First: </TextSymbolizer>
  [name1]
  <TextSymbolizer size="9" dy="-5"> Last: </TextSymbolizer>
  <TextSymbolizer face-name="DejaVu Sans Bold">[name2]</TextSymbolizer>
</TextSymbolizer>

Maybe <InnerTextSymbolizer> would be a better approach in terms of validating what attributes can only apply to the outer element?

Semi-related stuff:

  • If you do nesting of XML elements need to figure out whitespace handling...
  • Carto should really deprecate text-name: in favour of text-content: since it's evolved to include multiple fields as well as raw strings. Always trips me up :)
  • Any of the above would also let you do text-name: format("%0.1f", [HEIGHT]); style expressions within larger strings. Though so would text-name: "Height: " format("%0.1f", [HEIGHT]) "m";

@springmeyer
Copy link

@rcoup wins: wow - thanks for these excellent ideas. @jhollinger2 - want to digest and provide a new proposal?

@hollinger hollinger reopened this May 27, 2014
@hollinger
Copy link
Author

Thanks a lot for providing these suggestions!

First of all, I would second the notion of deprecating text-name to replace with text-content, although that does not necessarily need to be paired with this effort.

An big advantage of using pseudo-elements is that specific style attributes of those elements can be overriden within subsequent definitions without having to rewrite the whole list. I think there could be several approaches though.

Using ::before and ::after also falls in line with standard CSS. One downside to this approach is having to split up text content instead of being able to simply insert nodes into an arbitrary position inside the parent label.

Consider the following XML definition, which places one bold, red word in the middle of a label:

<TextSymbolizer face-name="DejaVu Sans">
  "Here is a "
  <Format fill="#ff0000">red</Format>
  " word."
</TextSymbolizer>

In Carto CSS, I think the most consise way to represent this using ::before and ::after elements would be:

#label {
  text-content: "Here is a ";
  text-face-name: "DejaVu Sans";
  text::after(1) {
    content: "red";
    face-name: "DejaVu Sans Bold";
    fill: #ff0000;
  }
  text::after(2) {
    content: " word.";
  }
}

I also note here, that this would generate an unneeded format node around the last part:

<TextSymbolizer face-name="DejaVu Sans">
  "Here is a "
  <Format fill="#ff0000">"red"</Format>
  <Format>" word."</Format>
</TextSymbolizer>

Adding logic here to avoid creating XML nodes when no attributes are set other than content would create the desired XML.

Another potential problem is that this syntax does not clearly distinguish between format and a layout. On the XML side, I don't see a good way to combine this into a single <InnerTextSymbolizer> node, because the two behave very differently. A format node simply changes the text appearance within the same text layout, while layout node creates an entirely separate layout that gets positioned independently of the other text.

I think that a solution to that could be to simply create either one or both types of XML element based on what attributes are defined within a psuedo-element.

The following combinations would result in XML rendering:


text::after(1) {
  content: 'text';
}
text

text::after(1) {
  content: 'text';
  size: 12;
}
<Format size="12">text</Format>

text::after(1) {
  content: 'text';
  dy: 8;
}
<Layout dy="8">text</Layout>

text::after(1) {
  content: 'text';
  size: 12;
  dy: 8;
}
<Layout dy="8">
  <Format size="12">text</Format>
</Layout>

Furthermore, nested elements could be supported as follows:

#style {
  text-face-name: 'DejaVu Sans';
  text-name: [name];
  text::before(1) {
    content: [alt];
    dy: 8;
  }
  text::before(1)::after(1) {
    content: 'first';
    fill: #0000ff;
  }
  text::before(1)::after(2) {
    content: 'second';
    size: 10;
  }
}
<TextSymbolizer face-name="DejaVu Sans">
  <Layout dy="8">
    [alt]
    <Format fill="#0000ff">"first"</Format>
    <Format size="10">"second"</Format>
  </Layout>
  [name]
</TextSymbolizer>

I think this syntax could still be a bit cumbersome, depending on what the user is trying to accomplish, but it would work.


The only other thought that I had is, rather than using ::before and ::after, combine the psuedo-element syntax with the ability to reference a format or layout style within the parent text. This could be done potentially with function call syntax.

For example:

#style {
  text-name: 'Here is a' format(1, 'red') 'word.'
  text::format(1) {
     fill: #ff0000;
  }
}

and

#style {
  text-face-name: 'DejaVu Sans';
  text-name:
    layout(1,
      [alt]
      format(1, 'first')
      format(2, 'second')
      'third'
    ) [name];
  text::layout(1) {
    dy: 8;
  }
  text::format(1) {
    fill: #0000ff;
  }
  text::format(2) {
    size: 10;
  }
}
<TextSymbolizer face-name="DejaVu Sans">
  <Layout dy="8">
    [alt]
    <Format fill="#0000ff">"first"</Format>
    <Format size="10">"second"</Format>
    "third"
  </Layout>
  [name]
</TextSymbolizer>

This would make the distinction between format and layout nodes more clearly defined and keep the text content itself all in one place. However, it does deviate more from standard CSS conventions, and the text-name string can get messy if a lot of nesting is happening.

Ultimately, sticking with ::before and ::after would probably be a simpler approach, but I would be okay with either.

@hollinger
Copy link
Author

Any further thoughts about the psuedo-element syntax?

I am planning to start some of the initial work on this soon. Also, some additions will need to be made in mapnik-reference to define which attributes can be set in Layout and Format nodes.

@springmeyer
Copy link

/cc @ajashton for thoughts. My gut is that its a step in the right direction but feels to complicated still.

@hollinger
Copy link
Author

Another suggestion about the distinction between format vs layout:

Since layout nodes are placed completely independently of the parent, it makes no difference where the <Layout>...</Layout> falls within the xml content of the TextSymbolizer definition. This is unlike format nodes, for which the order does make a difference in placement and rendering.

e.g. The following two text xml definitions will result in the exact same rendering:

<TextSymbolizer><Layout dy="8">[first]</Layout>[second]</TextSymbolizer>
<TextSymbolizer>[second]<Layout dy="8">[first]</Layout></TextSymbolizer>

By contast, these two definitions will render differently:

<TextSymbolizer><Format size="12">[first]</Format>[second]</TextSymbolizer>
<TextSymbolizer>[second]<Format size="12">[first]</Format></TextSymbolizer>

I think that it would help to eliminate some confusion if layouts and formats were handled differently in terms of Carto CSS. Layouts actually present a simpler case than formats, because any Layouts that are defined can just be appended to the end of the xml content inside the symbolizer.

Therefore, for layouts specifically, I would suggest representing a layout as a pseudo-element of it's own:

e.g.

#style {
  text-face-name: 'DejaVu Sans';
  text-name: [name];
  text::layout(1) {
    content: 'up';
    dy: 15;
  }
  text::layout(1)::layout(1) {
    content: 'up, right';
    dx: 15;
  }
}
<TextSymbolizer face-name="DejaVu Sans">
  [name]
  <Layout dy="15">
    "up"
    <Layout dx="15">"up, left"</Layout>
  </Layout>
</TextSymbolizer>

Note that the above example shows a nested Layout node, which I think might still be worth supporting on the grounds that it inherits attributes from the parent. The nested layout nodes would still always be appended to the end of the xml content inside the parent.

Format nodes will need to be handled differently. I think it could still work to represent formats with ::before and ::after, similar to what was discussed previously. Doing so would be made less complex by the fact that ::before and ::after would only ever create Format nodes, instead of having to determine Layout and/or Format based on what attributes are defined.

@nebulon42
Copy link
Collaborator

I'm not that happy with pseudo element syntax as it is quite similar to attachments and the extra : is easy to overlook or not obvious for newcomers. Hard to figure out when to use what.

I think the the formats and layouts have to be defined separately and referenced in the text-name string (content). Defining them separately enables to override them later and referencing them in a content string gives you overview. The syntax with ::before(1)::after(1) is too complex and you easily get lost in it.

Then there is the distinction between format and layout. Location of the layout reference is not important, but you should be able to nest layout references. So referencing them in the content string may lead to the false assumption that the order or location is important. I'm not sure if it is better to have them separate or similar as format references.

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

No branches or pull requests

4 participants