地図に文字を描くということについては色んなニーズがある。
現在は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);
}