]> mj.ucw.cz Git - leo.git/blob - sym-text.c
Initial support for LUA bindings in stylesheets
[leo.git] / sym-text.c
1 /*
2  *      Hic Est Leo -- Text Symbolizer
3  *
4  *      (c) 2014 Martin Mares <mj@ucw.cz>
5  */
6
7 #include "leo.h"
8 #include "osm.h"
9 #include "sym.h"
10 #include "map.h"
11
12 #include <ucw/stkstring.h>
13
14 #include <math.h>
15 #include <stdio.h>
16 #include <ft2build.h>
17 #include FT_FREETYPE_H
18 #include <pango/pangoft2.h>
19
20 /*** Fonts ***/
21
22 struct text_font {
23   const char *family;
24   double size;
25   osm_val_t weight;
26   osm_val_t style;
27   PangoFontDescription *pango_font_desc;
28   char key[1];
29 };
30
31 #define HASH_NODE struct text_font
32 #define HASH_PREFIX(x) text_font_##x
33 #define HASH_KEY_ENDSTRING key
34 #define HASH_WANT_LOOKUP
35 #define HASH_LOOKUP_DETECT_NEW
36 #define HASH_USE_POOL sym_mp
37 #include <ucw/hashtable.h>
38
39 static PangoFontMap *pango_font_map;
40 static PangoContext *pango_context;
41
42 static double pt_to_mm(double pt)
43 {
44   return pt / 72 * 25.4;
45 }
46
47 static double mm_to_pt(double mm)
48 {
49   return mm / 25.4 * 72;
50 }
51
52 static double pango_to_mm(double pango)
53 {
54   return pt_to_mm(pango / PANGO_SCALE);
55 }
56
57 static double mm_to_pango(double mm)
58 {
59   return mm_to_pt(mm) * PANGO_SCALE;
60 }
61
62 /*
63  * Pango tries to do pixel-based optimizations in low resolutions.
64  * Work around it by scaling everything up.
65  */
66 #define FONT_HACK_FACTOR 30
67
68 static struct text_font *font_get(struct text_font *req)
69 {
70   char *key = stk_printf("%s:%.6g:%u:%u", req->family, req->size, req->weight, req->style);
71   int is_new = 0;
72   struct text_font *font = text_font_lookup(key, &is_new);
73   if (is_new)
74     {
75       msg(L_DEBUG, "Loading font %s (size %.6g, weight %s, style %s)", req->family, req->size, osm_val_decode(req->weight), osm_val_decode(req->style));
76       font->family = mp_strdup(sym_mp, req->family);
77       font->size = req->size;
78       font->weight = req->weight;
79       font->style = req->style;
80
81       PangoFontDescription *desc;
82       desc = pango_font_description_new();
83       ASSERT(desc);
84       pango_font_description_set_family(desc, font->family);
85       if (font->weight != VALUE_NORMAL)
86         pango_font_description_set_weight(desc, PANGO_WEIGHT_BOLD);
87       if (font->style != VALUE_NORMAL)
88         pango_font_description_set_style(desc, PANGO_STYLE_ITALIC);
89       pango_font_description_set_size(desc, mm_to_pango(font->size));
90       font->pango_font_desc = desc;
91
92 #if 1
93       // FIXME
94       PangoFont *pfont = pango_font_map_load_font(pango_font_map, pango_context, desc);
95       ASSERT(pfont);
96       PangoFontDescription *d2 = pango_font_describe(pfont);
97       ASSERT(d2);
98       msg(L_DEBUG, "Font desc: %s", pango_font_description_to_string(d2));
99
100       PangoFontMetrics *fm = pango_font_get_metrics(pfont, NULL);
101       ASSERT(fm);
102       msg(L_DEBUG, "Font metrics: asc=%.6g desc=%.6g", pango_to_mm(pango_font_metrics_get_ascent(fm)), pango_to_mm(pango_font_metrics_get_descent(fm)));
103 #endif
104     }
105   return font;
106 }
107
108 static void font_init(void)
109 {
110   text_font_init();
111
112   pango_font_map = pango_ft2_font_map_new();
113   ASSERT(pango_font_map);
114   pango_ft2_font_map_set_resolution((PangoFT2FontMap *) pango_font_map, 72 * FONT_HACK_FACTOR, 72 * FONT_HACK_FACTOR);
115   pango_context = pango_font_map_create_context(PANGO_FONT_MAP(pango_font_map));
116   ASSERT(pango_context);
117 }
118
119 static void text_size(struct sym_text *st)
120 {
121   PangoLayout *layout = pango_layout_new(pango_context);
122   pango_layout_set_font_description(layout, st->font->pango_font_desc);
123   pango_layout_set_text(layout, osm_val_decode(st->text), -1);
124   pango_layout_context_changed(layout);
125
126   PangoRectangle ext;
127   pango_layout_get_extents(layout, NULL, &ext);
128   // st->tx = pango_to_mm(ext.x) / FONT_HACK_FACTOR;
129   // st->ty = pango_to_mm(ext.y) / FONT_HACK_FACTOR;
130   st->tw = pango_to_mm(ext.width) / FONT_HACK_FACTOR;
131   st->th = pango_to_mm(pango_layout_get_baseline(layout)) / FONT_HACK_FACTOR;
132   st->td = pango_to_mm(ext.height) / FONT_HACK_FACTOR - st->th;
133
134   g_object_unref(layout);
135 }
136
137 /*** Elimination of duplicate texts ***/
138
139 // FIXME: Get rid of globals
140 #define DUP_TILE_SIZE 10
141 static int dup_tiles_w, dup_tiles_h;
142 static struct sym_text **dup_tiles;
143
144 static void text_dup_init(void)
145 {
146   dup_tiles_w = 1 + page_map_width / DUP_TILE_SIZE;
147   dup_tiles_h = 1 + page_map_height / DUP_TILE_SIZE;
148   dup_tiles = xmalloc_zero(sizeof(struct sym_text *) * dup_tiles_w * dup_tiles_h);
149   msg(L_DEBUG, "Allocated text tiles: %u x %u", dup_tiles_w, dup_tiles_h);
150 }
151
152 static int text_dup_coord(int x, int y)
153 {
154   ASSERT(x >= 0 && x < dup_tiles_w && y >= 0 && y < dup_tiles_h);
155   return x + y * dup_tiles_w;
156 }
157
158 static double text_quad_dist(struct sym_text *a, struct sym_text *b)
159 {
160   double dx = a->x - b->x;
161   double dy = a->y - b->y;
162   return dx*dx + dy*dy;
163 }
164
165 static bool text_dup_detect(struct sym_text *t, struct style_info *si)
166 {
167   // Out-of-frame texts are dropped immediately
168   double x = t->x - page_offset_x;
169   double y = t->y - page_offset_y;
170   if (x < 0 || x >= page_map_width ||
171       y < 0 || y >= page_map_height)
172         return 0;
173
174   int tile_x = x / DUP_TILE_SIZE;
175   int tile_y = y / DUP_TILE_SIZE;
176
177   double radius = 0;
178   style_get_number(si, PROP_TEXT_DUP_THRESHOLD, &radius);
179   if (radius)
180     {
181       // A rather simple-minded algorithm, but believed to be efficient enough.
182       int r_tiles = 1 + radius / DUP_TILE_SIZE;
183       int x_start = MAX(0, tile_x - r_tiles);
184       int x_stop = MIN(dup_tiles_w - 1, tile_x + r_tiles);
185       int y_start = MAX(0, tile_y - r_tiles);
186       int y_stop = MIN(dup_tiles_h - 1, tile_y + r_tiles);
187       for (int x = x_start; x <= x_stop; x++)
188         for (int y = y_start; y <= y_stop; y++)
189           {
190             for (struct sym_text *d = dup_tiles[text_dup_coord(x, y)]; d; d = d->next_in_tile)
191               if (d->text == t->text &&
192                   text_quad_dist(d, t) <= radius*radius &&
193                   d->text_color == t->text_color &&
194                   d->font == t->font)
195                 {
196                   t->next_duplicate = d->next_duplicate;
197                   d->next_duplicate = t;
198                   return 0;
199                 }
200           }
201     }
202
203   int tile_i = text_dup_coord(tile_x, tile_y);
204   t->next_in_tile = dup_tiles[tile_i];
205   dup_tiles[tile_i] = t;
206   return 1;
207 }
208
209 /*** Core of the symbolizer ***/
210
211 static void prepare_text_element(struct sym_text *t, struct svg *svg)
212 {
213   struct text_font *font = t->font;
214   svg_push_element(svg, "text");
215   svg_set_attr_dimen(svg, "x", t->x);
216   svg_set_attr_dimen(svg, "y", t->y);
217   svg_set_attr(svg, "font-family", font->family);
218   svg_set_attr_dimen(svg, "font-size", font->size);
219   if (font->weight != VALUE_NORMAL)
220     svg_set_attr(svg, "font-weight", osm_val_decode(font->weight));
221   if (font->style != VALUE_NORMAL)
222     svg_set_attr(svg, "font-style", osm_val_decode(font->style));
223 }
224
225 static void sym_text_draw(struct symbol *sym, struct svg *svg)
226 {
227   struct sym_text *t = (struct sym_text *) sym;
228
229   if (t->next_duplicate)
230     {
231       // If there is a cluster of duplicate texts, average their positions
232       double sx = 0, sy = 0;
233       uns nn = 0;
234       for (struct sym_text *u = t; u; u = u->next_duplicate)
235         {
236           sx += u->x;
237           sy += u->y;
238           nn++;
239         }
240       t->x = sx / nn;
241       t->y = sy / nn;
242     }
243
244   bool use_group = 0;
245   bool want_rotate = (fabs(t->rotate) > 1e-5);
246   if (t->opacity != 1 || want_rotate)
247     {
248       use_group = 1;
249       svg_push_element(svg, "g");
250       if (t->opacity != 1)
251         svg_set_attr_float(svg, "opacity", t->opacity);
252       if (want_rotate)
253         svg_set_attr_format(svg, "transform", "rotate(%.2f %s %s)", -t->rotate, svg_format_dimen(svg, t->x), svg_format_dimen(svg, t->y));
254     }
255
256   if (t->halo_radius)
257     {
258       prepare_text_element(t, svg);
259       svg_set_attr(svg, "fill", "none");
260       svg_set_attr_color(svg, "stroke", t->halo_color);
261       svg_set_attr_dimen(svg, "stroke-width", t->halo_radius);
262       svg_set_attr(svg, "stroke-linecap", "round");
263       svg_set_attr(svg, "stroke-linejoin", "round");
264       if (t->halo_opacity != 1)
265         svg_set_attr_float(svg, "stroke-opacity", t->halo_opacity);
266       svg_push_chars(svg)->name = osm_val_decode(t->text);
267       svg_pop(svg);
268       svg_pop(svg);
269     }
270
271   prepare_text_element(t, svg);
272   svg_set_attr_color(svg, "fill", t->text_color);
273   svg_set_attr(svg, "stroke", "none");
274   svg_push_chars(svg)->name = osm_val_decode(t->text);
275   svg_pop(svg);
276   svg_pop(svg);
277
278 #if 0
279   // Draw bounding box for debugging
280   svg_push_element(svg, "rect");
281   svg_set_attr(svg, "fill", "none");
282   svg_set_attr_color(svg, "stroke", 0x0000ff);
283   svg_set_attr_dimen(svg, "stroke-width", 0.2);
284   svg_set_attr_dimen(svg, "x", t->x);
285   svg_set_attr_dimen(svg, "y", t->y - t->th);
286   svg_set_attr_dimen(svg, "width", t->tw);
287   svg_set_attr_dimen(svg, "height", t->th + t->td);
288   svg_pop(svg);
289
290   svg_push_element(svg, "line");
291   svg_set_attr(svg, "fill", "none");
292   svg_set_attr_color(svg, "stroke", 0x0000ff);
293   svg_set_attr_dimen(svg, "stroke-width", 0.2);
294   svg_set_attr_dimen(svg, "x1", t->x);
295   svg_set_attr_dimen(svg, "y1", t->y);
296   svg_set_attr_dimen(svg, "x2", t->x + t->tw);
297   svg_set_attr_dimen(svg, "y2", t->y);
298   svg_pop(svg);
299 #endif
300
301   if (use_group)
302     svg_pop(svg);
303 }
304
305 static osm_val_t get_text(struct osm_object *o, struct style_info *si)
306 {
307   struct style_prop *prop = style_get_and_check(si, PROP_TEXT, (1 << PROP_TYPE_STRING) | (1 << PROP_TYPE_IDENT));
308   if (!prop)
309     return 0;
310
311   if (prop->type == PROP_TYPE_IDENT && prop->val.id == VALUE_AUTO)
312     {
313       static const osm_key_t auto_text_keys[] = {
314         KEY_NAME_CZ,            // FIXME: This should be configurable
315         KEY_NAME,
316         KEY_REF,
317         KEY_OPERATOR,
318         KEY_BRAND,
319         KEY_ADDR_HOUSENUMBER,
320       };
321       for (uns i=0; i < ARRAY_SIZE(auto_text_keys); i++)
322         {
323           osm_val_t val = osm_obj_find_tag(o, auto_text_keys[i]);
324           if (val)
325             return val;
326         }
327       return 0;
328     }
329
330   return osm_obj_find_tag(o, osm_key_encode(osm_val_decode(prop->val.id)));
331 }
332
333 static void get_text_attrs(struct sym_text *st, struct style_info *si)
334 {
335   struct osm_object *o = st->s.o;
336   if (o->type == OSM_TYPE_WAY && osm_way_cyclic_p((struct osm_way *) o))
337     st->text_color = 0xc0c0c0;  // FIXME: This is an ugly hack, do we need it?
338   else
339     st->text_color = 0xffffff;
340   style_get_color(si, PROP_TEXT_COLOR, &st->text_color);
341
342   struct text_font f = {
343     .family = "Helvetica",
344     .size = pt_to_mm(8),
345   };
346   osm_val_t fam = style_get_string(si, PROP_FONT_FAMILY);
347   if (fam)
348     f.family = osm_val_decode(fam);
349   style_get_number(si, PROP_FONT_SIZE, &f.size);
350   f.weight = style_get_ident(si, PROP_FONT_WEIGHT);
351   if (!f.weight)
352     f.weight = VALUE_NORMAL;
353   if (f.weight != VALUE_NORMAL && f.weight != VALUE_BOLD)
354     {
355       osm_obj_warn(o, "Unknown font-weight %s", osm_val_decode(f.weight));
356       f.weight = VALUE_NORMAL;
357     }
358   f.style = style_get_ident(si, PROP_FONT_STYLE);
359   if (!f.style)
360     f.style = VALUE_NORMAL;
361   if (f.style != VALUE_NORMAL && f.style != VALUE_ITALIC)
362     {
363       osm_obj_warn(o, "Unknown font-style %s", osm_val_decode(f.weight));
364       f.style = VALUE_NORMAL;
365     }
366   st->font = font_get(&f);
367
368   st->opacity = 1;
369   style_get_number(si, PROP_TEXT_OPACITY, &st->opacity);
370
371   st->halo_color = st->text_color ^ 0xffffff;
372   style_get_color(si, PROP_TEXT_HALO_COLOR, &st->halo_color);
373   st->halo_radius = 0;
374   style_get_number(si, PROP_TEXT_HALO_RADIUS, &st->halo_radius);
375   st->halo_opacity = 1;
376   style_get_number(si, PROP_TEXT_HALO_OPACITY, &st->halo_opacity);
377
378   double dx = 0, dy = 0;
379   style_get_number(si, PROP_TEXT_OFFSET_X, &dx);
380   style_get_number(si, PROP_TEXT_OFFSET_Y, &dy);
381   style_get_number(si, PROP_TEXT_OFFSET, &dy);
382   st->x += dx;
383   st->y -= dy;
384 }
385
386 static void text_fix_placement(struct sym_text *st)
387 {
388   // Fix texts which do not fit on the paper
389   st->x = MIN(st->x, page_offset_x + page_map_width - st->tw);
390   st->x = MAX(st->x, page_offset_x);
391   st->y = MIN(st->y, page_offset_y + page_map_height - st->th);
392   st->y = MAX(st->y, page_offset_y + st->td);
393 }
394
395 static void sym_text_node(struct osm_object *o, struct style_info *si, osm_val_t text)
396 {
397   struct osm_node *n = (struct osm_node *) o;
398
399   struct sym_text *st = sym_text_new(o);
400   st->text = text;
401   st->x = n->x;
402   st->y = n->y;
403
404   get_text_attrs(st, si);
405   text_size(st);
406
407   osm_val_t ah = style_get_ident(si, PROP_TEXT_ANCHOR_HORIZONTAL);
408   switch (ah)
409     {
410     case VALUE_LEFT:
411       st->x -= st->tw;
412       break;
413     case VALUE_CENTER:
414       st->x -= st->tw / 2;
415       break;
416     case 0:
417     case VALUE_RIGHT:
418       break;
419     default:
420       osm_obj_warn(o, "Unknown text-anchor-horizontal: %s", osm_val_decode(ah));
421     }
422
423   osm_val_t av = style_get_ident(si, PROP_TEXT_ANCHOR_VERTICAL);
424   switch (av)
425     {
426     case VALUE_ABOVE:           // FIXME: What's the difference between above and top?
427     case VALUE_TOP:
428       st->y -= st->td;
429       break;
430     case VALUE_CENTER:
431       st->y -= (st->th + st->td) / 2;
432       // Fall thru
433     case 0:
434     case VALUE_BOTTOM:
435     case VALUE_BELOW:
436       st->y += st->th;
437       break;
438     default:
439       osm_obj_warn(o, "Unknown text-anchor-vertical: %s", osm_val_decode(av));
440     }
441
442   text_fix_placement(st);
443   if (!text_dup_detect(st, si))
444     {
445       msg(L_DEBUG, "Text <%s> dropped as duplicate", osm_val_decode(text));
446       return;
447     }
448
449   sym_plan(&st->s, sym_zindex(o, si, 5));
450 }
451
452 static void sym_text_center(struct osm_object *o, struct style_info *si, osm_val_t text, double x, double y)
453 {
454   struct sym_text *st = sym_text_new(o);
455   st->text = text;
456   st->x = x;
457   st->y = y;
458
459   get_text_attrs(st, si);
460   text_size(st);
461   st->x -= st->tw / 2;
462   st->y += st->th - (st->th + st->td) / 2;
463   text_fix_placement(st);
464   sym_plan(&st->s, sym_zindex(o, si, 4.9));
465 }
466
467 static void sym_text_way(struct osm_object *o, struct style_info *si, osm_val_t text)
468 {
469   double x, y;
470   osm_val_t tp = style_get_ident(si, PROP_TEXT_POSITION);
471
472   switch (tp)
473     {
474     case VALUE_CENTER:
475       if (osm_obj_center(o, &x, &y))
476         sym_text_center(o, si, text, x, y);
477       break;
478     case VALUE_LINE:
479       // FIXME
480     default:
481       osm_obj_warn(o, "Unknown text-position: %s", osm_val_decode(tp));
482     }
483 }
484
485 static void sym_text_mpg(struct osm_object *o, struct style_info *si, osm_val_t text)
486 {
487   double x, y;
488   osm_val_t tp = style_get_ident(si, PROP_TEXT_POSITION);
489
490   switch (tp)
491     {
492     case VALUE_CENTER:
493       if (osm_obj_center(o, &x, &y))
494         sym_text_center(o, si, text, x, y);
495       break;
496     case VALUE_LINE:
497       // FIXME
498     default:
499       osm_obj_warn(o, "Unknown text-position: %s", osm_val_decode(tp));
500     }
501 }
502
503 static void sym_text_gen(struct osm_object *o, struct style_info *si, struct svg *svg UNUSED)
504 {
505   osm_val_t text = get_text(o, si);
506   if (!text)
507     return;
508
509   switch (o->type)
510     {
511     case OSM_TYPE_NODE:
512       sym_text_node(o, si, text);
513       break;
514     case OSM_TYPE_WAY:
515       sym_text_way(o, si, text);
516       break;
517     case OSM_TYPE_MULTIPOLYGON:
518       sym_text_mpg(o, si, text);
519       break;
520     default:
521       osm_obj_warn(o, "Text symbolizer does not support this object type");
522       return;
523     }
524 }
525
526 static void sym_text_init(void)
527 {
528   font_init();
529   text_dup_init();
530 }
531
532 struct symbolizer symbolizer_text = {
533   .name = "text",
534   .draw = sym_text_draw,
535   .gen = sym_text_gen,
536   .init = sym_text_init,
537 };
538
539 struct sym_text *sym_text_new(struct osm_object *o)
540 {
541   return sym_new(SYMBOLIZER_TEXT, o, sizeof(struct sym_text));
542 }
543
544 // FIXME: Hack
545 void scale_text(struct svg *svg, double x, double y, osm_val_t text)
546 {
547   struct sym_text *st = sym_text_new(NULL);
548
549   struct text_font f = {
550     .family = "Times",
551     .weight = VALUE_NORMAL,
552     .style = VALUE_NORMAL,
553     .size = pt_to_mm(10),
554   };
555
556   st->text = text;
557   st->text_color = 0;
558   st->x = x;
559   st->y = y;
560   st->font = font_get(&f);
561   st->opacity = 1;
562   st->halo_color = 0xffffff;
563   st->halo_radius = 0.8;
564   st->halo_opacity = 1;
565   text_size(st);
566   st->x -= st->tw / 2;
567   sym_text_draw(&st->s, svg);
568 }