地図に文字を描くということについては色んなニーズがある。
現在は1文字ごとに角度を変えて描画しているのではなく、全文字を描けるだけの直線区間に描いている。 水平、垂直ではなく、角度のある描画が必要である。
1文字ずつ角度を変えて描画できればその方がよい。
これまではこれらのニーズに自前で対応してきたため、プログラム行数が嵩んでいる。 改めて、できるだけ楽な方法を探りたい。
現在、インターネットでは UTF-8 が主流になっている。 自分がホームページを開設したのは15以上前であり、ファイルサイズを小さくするために、Shift-JIS を使っていた。
地図システムとしては日本地図が主体のため、日本語の比重が高い。 文字列をコード化しているため、その辞書をメモリに常駐する。メモリサイズはできるだけ小さくするには、 日本語(Shift-JISコードだけ)中心の場合、Shift-JIS、のACSIIコードだけまたは、 Shift-JISに含まれないマルチバイトコードの場合 UTF-8 とした時期もある。
そこまでファイルサイズに拘るのはやめ、現在は、UTF-16コード一本にしている。 2バイト固定長であり、C# とか Java の内部文字コードでもあるため、プログラム的には扱いやすい。
日本語が中心であれば、Shift-JISコードとほぼ同じサイズとなる。 ASCII文字が多い場合は、Shify-JISコードよりファイルサイズが大きくなる。
UTF-16BE(Big Endian)と UTF-16LE(Little Endian)があり、 Java は Big Endian、C# は Little Endian であるため、注意が必要になる。
C# の場合、Graphics.DrawStringメソッドを使えば、文字列を複数行に分けて描画できる[1]。 しかし、Graphics.DrawStringメソッドで GraphicsPath が使えなければ、縁取り文字が描けない。
これまでは、複数行分割は自前で行い、GraphicsPath で縁取り文字を描いてきた[2]。
改めて、調べたが、結局、これまでのプログラムを手直しして、いくぶん、分かりやすくした。 描画結果を下に示す。
地名の場合は、描画位置に多少のずれがあっても問題はないが、 多くの地物はアイコンの下に名称を描画するため、的確な位置に描画しないと見栄えが悪くなる。
OSMデータの座標位置を (x, y) とする。タイルの左上を原点とする画素単位の値である。
この座標(x, y) を中心としてアイコンを描画するには次のようにする。 引数に指定するのは、左上の座標と幅、高さである。
g.DrawImage(img, x - img.Width/2, y - img.Height/2, img.Width, img.Height);
文字列描画の位置決めはアイコンに比べるとはるかに面倒である。 複数行に分割した場合、描画も複数回に分かれる。 縁取り文字を描画するため、GraphicsPath に登録する際に座標位置を指定する。
アイコンの場合と同様に、文字列描画領域の左上の座標を指定する。 従って、予め、文字列描画領域を求める必要がある。
アイコンを描画しない場合は、複数行の文字列全体の中心が座標(x、y)に一致させる。 アイコンを描画する場合は、アイコンの真下に文字列が描画されるように、 文字列の描画位置を下にずらす。
文字列の描画領域は Graphics#MeasureStringメソッドで得られるが、 マージンがあるため、実際の描画領域とピッタリ一致するわけではない。
以上のことから、位置決めは面倒であるが、C# でも Android Java でも経験済みであるため、 これらを参考にして、なるべく分かりやすくしたい。
以前のプログラムに次のメソッドがあった。もし、これが複数行に対して有効に働くならば、 その後のプログラムよりも簡単である。
まずは試してみる。
public bool DrawString(Graphics g, Pnt point, string fontname, FontStyle fontstyle, Brush brush, float fontsize, int dy, int margin, string name0, Pen penHalo=null) { if (name0 == null || fontsize <= 0) return false; float x = point.xf; float y = point.yf; Font fnt = new Font(fontname, fontsize/1.2f, fontstyle); var name = Wrap(name0); SizeF size = g.MeasureString(name, fnt); float w = size.Width + 1.8f; float h = size.Height + 0.1f; Rectangle rect = new Rectangle( // 描画領域 (int)(x-w/2), dy==0 ? (int)(y-h/2) : (int)(y+dy/1.33f), (int)w, (int)h); Rectangle rectMargin = new Rectangle( // 描画領域+マージン rect.X-margin, rect.Y-margin, rect.Width+margin*2, rect.Height+margin*2); foreach (Rectangle r in listRect) { if (r.IntersectsWith(rectMargin)) { return false; // 重なり回避のため描画しない } } GraphicsPath gp = new GraphicsPath(); gp.AddString (name, new FontFamily(fontname), (int)fontstyle, fontsize, rect, StringFormat.GenericDefault); if (penHalo != null) g.DrawPath(penHalo, gp); g.FillPath(brush, gp); listRect.Add(rect); // 地物名が地名に重ならないように常に地名も登録 return true; }
まず、MesureStringが複数行に対応しているかどうかが心配だったが、対応していることが確認できた。
最終的には、以下のようにした。MeasureStringは大きめの値が返されることから、0.8がけとした。 駅のアイコンと駅名描画で所望の結果が得られることを確認した。 今後、描画領域の幅と高さについての補正は修正するかも知れない。 また、この段階では、重なり回避は行っていない。地名同士の重なり回避は行っている。
public void DrawText(Graphics g, Font font, Brush br, float halo, TileToRender tile, string text, float dy, bool fRect) { if (text == null) return; var name = Wrap(text); SizeF size = g.MeasureString(name, font); float w = size.Width * 0.8f; float h = size.Height * 0.8f; double fact = tile.fact; double xpx = tile.xpx; double ypx = tile.ypx; float x = (float) (xc * fact - xpx); // 中心X座標 float y = (float) (yc * fact - ypx); // 中心Y座標 PointF pnt = new PointF(x-w/2, dy==0 ? y-h/2 : y+dy); GraphicsPath gp = new GraphicsPath(); gp.AddString(name, font.FontFamily, (int)font.Style, font.Size, pnt, StringFormat.GenericDefault); Pen drawPen = new Pen(Color.White, halo); g.DrawPath(drawPen, gp); // パスの線分を描画(縁取り) g.FillPath(br, gp); }
重なり回避を入れた最終プログラムを以下に示す。
public void DrawText(Graphics g, Font font, Brush br, float halo, TileToRender tile, string text, float dy=0, bool fRect=true) { if (text == null) return; var name = Wrap(text); SizeF size = g.MeasureString(name, font); float w = size.Width * 0.8f; float h = size.Height * 0.8f; double fact = tile.fact; double xpx = tile.xpx; double ypx = tile.ypx; float x = (float) (xc * fact - xpx); // 中心X座標 float y = (float) (yc * fact - ypx); // 中心Y座標 PointF pnt = new PointF(x-w/2, dy==0 ? y-h/2 : y+dy); RectangleF rect = new RectangleF(pnt.X, pnt.Y, w, h); GraphicsPath gp = new GraphicsPath(); gp.AddString(name, font.FontFamily, (int)font.Style, font.Size, pnt, StringFormat.GenericDefault); foreach (RectangleF r in tile.listRect) { if (r.IntersectsWith(rect)) return; // 重なり回避のため描画しない } tile.listRect.Add(rect); Pen drawPen = new Pen(Color.White, halo); g.DrawPath(drawPen, gp); // パスの線分を描画(縁取り) g.FillPath(br, gp); }