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