トップ地図システム > 地図表示システム[C#]

地図表示システム[C#]

1.最初の一歩

まずは、地図のスクロールは左上の矢印をクリックすることにより実行するようにした。 しかし、実際使ってみると、使い勝手は良くなかった。 次は、マウスカーソルでドラッグできるようにしてみよう。

using System;
using System.Drawing;
using System.IO;
using System.Windows.Forms;

class GIS : Form {
    const int NX = 3;
    const int NY = 2;
    const string DIR = "c:/osm/tiles/";
    StatusBarPanel sbp0, sbp1, sbp2;
    PictureBox[] pbs = new PictureBox[NX*NY];
    int Zoom = 6, X = 55, Y = 24;

    public GIS() {
        Text = "タイル地図[(c)OpenStreetMap]表示";
        Size = new Size(256*NX+8, 256*NY+50);
        Panel mainPanel = new Panel() { Dock = DockStyle.Fill };
        Panel ctrlPanel = new Panel() { Dock = DockStyle.Bottom, Height = 25 };
        this.Controls.AddRange(new Panel[] { mainPanel, ctrlPanel });
        StatusBar statusBar = new StatusBar() { Dock = DockStyle.Fill, ShowPanels = true };
        ctrlPanel.Font = new Font("Thoma", 10);
        sbp0 = new StatusBarPanel() { Width = 50 };
        sbp1 = new StatusBarPanel() { Width = 50 };
        sbp2 = new StatusBarPanel() { AutoSize = StatusBarPanelAutoSize.Spring };
        statusBar.Panels.AddRange(new StatusBarPanel[] { sbp0, sbp1, sbp2 });
        ctrlPanel.Controls.Add(statusBar);
        for (int nY = 0; nY < NY; nY++) {
            for (int nX = 0; nX < NX; nX++) {
                pbs[nY*NX+nX] = new PictureBox();
                pbs[nY*NX+nX].Bounds = new Rectangle(nX*256, nY*256, 256, 256);
                mainPanel.Controls.Add(pbs[nY*NX+nX]);
            }
        }
        DrawMap();      // 初期描画

        NumericUpDown numUpDown = new NumericUpDown() {
            Dock = DockStyle.Left, Width = 40,
            Minimum = 2, Maximum = 18, Value = Zoom
        };
        numUpDown.TextChanged += (s, e) => {
            int prevZoom = Zoom;
            Zoom = (int)numUpDown.Value;
            double pow = Math.Pow(2, Zoom-prevZoom);
            X = (int)(X*pow+0.5);
            Y = (int)(Y*pow+0.5);
            DrawMap();
        };
        ctrlPanel.Controls.Add(numUpDown);
        pbs[0].Paint += (s,e) => {
            Graphics g = e.Graphics;
            Font font = new Font("Thoma", 16, FontStyle.Bold);
            g.DrawString("↑", font, Brushes.Blue, 20, 0);
            g.DrawString("← →", font, Brushes.Blue, 0, 20);
            g.DrawString("↓", font, Brushes.Blue, 20, 40);
        };
        pbs[0].MouseClick += (s, e) => {
            this.Text = String.Format("({0}, {1})", e.X, e.Y);  // クライアント座標
            if (e.Y < 25 && 25 < e.X && e.X < 40) Y++;
            else if (e.Y > 35 && 25 < e.X && e.X < 40) Y--;
            else if (e.X < 25 && 25 < e.Y && e.Y < 40) X++;
            else if (e.X > 40 && 25 < e.Y && e.Y < 40) X--;
            else return;
            DrawMap();
        };
    }

    void DrawMap() {
        for (int nY = 0; nY < NY; nY++) {
            for (int nX = 0; nX < NX; nX++) {
                string path = DIR + Zoom + "/" + (X+nX) + "/" + (Y+nY) + ".png";
                pbs[nY*NX+nX].Image = Image.FromFile(File.Exists(path) ? path : "error.png");
            }
        }
        sbp0.Text = " X: " + X;
        sbp1.Text = " Y: " + Y;
    }
}

class Program {
    [STAThread]
    static void Main() {
        Application.Run(new GIS());
    }
}

2.マウスでドラッグさせる

今度は JavaScript版と同じようにマウスでドラッグするように変更した。 やはり、想像した通り、JavaScriptよりもプログラムは相当簡単になった。

地図中心点(☆マーク)を表示できるようにした。左下のチェックボックスでオンオフをコントロールする。

現在は、Zoom, X, Y は画面左上のタイル画像を表す。ここは、中心点のタイル画像に変えた方がいいかも知れない。 あるいは、現在の状態はダイアログボックスで詳しく表示するようにしたい。

using System;
using System.Drawing;
using System.IO;
using System.Windows.Forms;

class MapPanel : Panel {
    public MapPanel() {
        this.DoubleBuffered = true;
    }
}

class GIS : Form {
    const string DIR = "c:/osm/tiles/";
    const int NX = 3, NY = 2;
    MapPanel mainPanel;
    CheckBox cbCenter;
    StatusBarPanel sbp0, sbp1, sbp2, sbp3;
    int Zoom = 9, X = 454, Y = 201;
    int offX = 0, offY = 0;
    int MarkX = -1, MarkY = -1;

    public GIS() {
        Text = "タイル地図表示";
        Size = new Size(256*NX+8, 256*NY+50);

        mainPanel = new MapPanel() { Dock = DockStyle.Fill };
        mainPanel.Paint     += (s,e) => { paintMap(e.Graphics); };
        mainPanel.MouseDown += (s,e) => { MarkX = e.X; MarkY = e.Y; };
        mainPanel.MouseUp   += (s,e) => { MarkX =  -1; MarkY =  -1; };
        mainPanel.MouseMove += (s,e) => { 
            if (MarkX < 0) return;
            offX -= e.X - MarkX;   // 移動量(微小)
            offY -= e.Y - MarkY;   // 移動量(微小)
            MarkX = e.X;
            MarkY = e.Y;
            if (offX > 0 || offX < -256 || offY > 0 || offY < -256) {
                if (offX > 0) { X++; offX -= 256; }
                else if (offX < -256) { X--; offX += 256; }
                if (offY > 0) { Y++; offY -= 256; }
                else if (offY < -256) { Y--; offY += 256; }
            }
            mainPanel.Refresh();
        };

        Panel ctrlPanel = new Panel() { Dock = DockStyle.Bottom, Height = 25 };
        this.Controls.AddRange(new Panel[] { mainPanel, ctrlPanel });

        Button btnPlus = new Button() { Text = "+", Bounds = new Rectangle(3,3,25,25) };
        btnPlus.Font = new Font("Thoma", 16);
        btnPlus.Click += new EventHandler(buttonClick);
        Button btnMinus = new Button() { Text = "−", Bounds = new Rectangle(3,29,25,24) };
        btnMinus.Font = new Font("Thoma", 16);
        btnMinus.Click += new EventHandler(buttonClick);
        mainPanel.Controls.AddRange(new Button[] { btnPlus, btnMinus });

        StatusBar statusBar = new StatusBar() { Dock=DockStyle.Fill, ShowPanels=true };
        ctrlPanel.Font = new Font("Thoma", 10);
        sbp0 = new StatusBarPanel() { Width = 70 };
        sbp1 = new StatusBarPanel() { Width = 70 };
        sbp2 = new StatusBarPanel() { Width = 70 };
        sbp3 = new StatusBarPanel() { AutoSize = StatusBarPanelAutoSize.Spring };
        statusBar.Panels.AddRange(new StatusBarPanel[] { sbp0, sbp1, sbp2 });

        cbCenter = new CheckBox() { 
            Text = "中心点", 
            Width = 65, 
            Dock = DockStyle.Left
        };
        cbCenter.CheckedChanged += (s,e) => { mainPanel.Refresh(); };

        ctrlPanel.Controls.AddRange(new Control[] { statusBar, cbCenter });

    }

    void buttonClick(object sender, EventArgs e) {
        double x, y;
        Button btn = (Button)sender;
        if (btn.Text == "+" && Zoom < 18) {
            x = (X+offX/256.0)*2 + NX/2.0;      // 新画像の左端のX座標
            y = (Y+offY/256.0)*2 + NY/2.0;      // 新画像の上端のY座標
            Zoom++;
        } else if (Zoom > 2) {
            x = (X+offX/256.0)/2 - NX/4.0;      // 新画像の左端のX座標
            y = (Y+offY/256.0)/2 - NY/4.0;      // 新画像の上端のY座標
            Zoom--;
        } else {
            return;
        }
        X = (int)x;
        Y = (int)y;
        offX = (int)((x-X)*256);
        offY = (int)((y-Y)*256);
        this.mainPanel.Refresh();
    }

    void paintMap(Graphics g) {
        Font font = new Font("Thoma",10);
        for (int nY = -1; nY <= NY; nY++) {
            for (int nX = -1; nX <= NX; nX++) {
                string path = DIR + Zoom + "/" + (X+nX) + "/" + (Y+nY) + ".png";
                if (!File.Exists(path)) path = DIR + "sea.png";
                using (Image img = Image.FromFile(path)) {
                  g.DrawImage(img, 256*nX-offX, 256*nY-offY, 256, 256);
                }
            }
        }
        if (cbCenter.Checked) {
            using (Image img = Image.FromFile("star.ico")) {
              g.DrawImage(img, 256*NX/2, 256*NY/2, 16, 16);
            }
        }
        g.DrawString("(c) OpenStreetMap", font, Brushes.Black, 256*NX-120, 256*NY-20);
        sbp0.Text = " Zoom: " + Zoom;
        sbp1.Text = " X: " + X;
        sbp2.Text = " Y: " + Y;
    }
}

class Program {
    [STAThread]
    static void Main() {
        Application.Run(new GIS());
    }
}

Image.FromFileメソッドで画像ファイルを読み込んで表示する場合、表示中はファイルを更新したり、 削除することができない。 これが不都合な場合、Image.FromStreamメソッドを使用する。

    FileStream fs = new FileStream(path, FileMode.Open, FileAccess.Read);
    Image img = Image.FromStream(fs);
    fs.Close();

3.改良版

今後、地図システムとして機能を拡張する場合、地図表示機能は MapClass に移す方がよい。

上のプログラムでは、地図表示画面サイズを NX*NYタイルとしているが、 ウィンドウサイズの変更に追従させた方がよい。 中心点を動かさず、ClientSize から新しい表示領域を求め、これをカバーするタイル画像を求めて表示する。

改良したプログラムを下に示す。メニューは準備のみで、項目はまだない。 タイル画像ファイル読み込み回数を減らすために OrderdDictionaryにタイル画像を登録するようにしたが、 前バージョンと比較して差は感じられなかった。 スクロール時に、毎回読み込むことにしても、ディスクキャッシュされるため、読み込み時間は小さいためであろう。 プログラム行数の増加は僅かであるから、少なくとも当面はOrderdDictionaryを使用したままとする。

using System;
using System.Collections.Specialized;   // OrderedDictionary;
using System.Drawing;
using System.IO;
using System.Windows.Forms;

class MapPanel : Panel {
    const string DIR = "c:/osm/tiles/";
    Image imgStar = Image.FromFile("star.ico");
    Image imgSea  = Image.FromFile(DIR + "sea.png");
    OrderedDictionary dictTiles = new OrderedDictionary();
    const int DictSize = 20;
    GIS gis;
    int W, H;                           // 地図表示領域の幅、高さ(単位:画素)
    int Zoom = 14;
    int CX = 14540*256, CY = 6458*256;  // 表示中心のXY座標(単位:画素)

    public MapPanel(GIS gis) {
        int MarkX = -1, MarkY = -1;
        this.gis = gis; 
        DoubleBuffered = true;

        Button btnPlus = new Button() { Text = "+", Bounds = new Rectangle(3,3,25,25) };
        btnPlus.Font = new Font("Thoma", 16);
        btnPlus.Click += new EventHandler(buttonClick);
        Button btnMinus = new Button() { Text = "−", Bounds = new Rectangle(3,29,25,24) };
        btnMinus.Font = new Font("Thoma", 16);
        btnMinus.Click += new EventHandler(buttonClick);
        Controls.AddRange(new Button[] { btnPlus, btnMinus });

        Paint     += (s,e) => { paintMap(e.Graphics); };
        MouseDown += (s,e) => { MarkX = e.X; MarkY = e.Y; };
        MouseUp   += (s,e) => { MarkX =  -1; MarkY =  -1; };
        MouseMove += (s,e) => { 
            if (MarkX < 0) return;
            CX -= e.X - MarkX; 
            CY -= e.Y - MarkY;
            MarkX = e.X;
            MarkY = e.Y;
            Refresh();
        };
    }

    protected override void OnResize(EventArgs e) {
        base.OnResize(e);
        W = ClientSize.Width;
        H = ClientSize.Height;
        Refresh();
    }

    void buttonClick(object sender, EventArgs e) {
        Button btn = (Button)sender;
        if (btn.Text == "+" && Zoom < 18) { Zoom++; CX *= 2; CY *= 2; }
        else if (Zoom > 2) { Zoom--; CX /= 2; CY /= 2; }
        else { return; }
        Refresh();
    }

    void paintMap(Graphics g) {         // (CX-W/2, CY-H/2)〜(CX+W/2, CY+H/2)
        Font font = new Font("Thoma", 10);
        int BX = (CX - W/2) / 256;              // 切り捨て
        int EX = (CX + W/2 + 255) / 256;        // 切り上げ
        int BY = (CY - H/2) / 256;
        int EY = (CY + H/2 + 255) / 256;
        int offX = (CX - W/2) % 256;
        int offY = (CY - H/2) % 256;
        for (int nY = BY; nY <= EY; nY++) {
            for (int nX = BX; nX <= EX; nX++) {
                string path = DIR + Zoom + "/" + nX + "/" + nY + ".png";
                Image img = imgSea;
                if (File.Exists(path)) {
                    if (!dictTiles.Contains(path)) {
                        if (dictTiles.Count > DictSize) dictTiles.RemoveAt(0);
                        FileStream fs = new FileStream(path, FileMode.Open, FileAccess.Read);
                        dictTiles[path] = Image.FromStream(fs);
                        fs.Close();
                    }
                    img = (Image)dictTiles[path];
                }
                g.DrawImage(img, 256*(nX-BX)-offX, 256*(nY-BY)-offY, 256, 256);
            }
        }
        if (gis.cbCenter.Checked) {
          g.DrawImage(imgStar, W/2, H/2, 16, 16);     // 中心点(☆)表示
        }
        g.DrawString("(c) OpenStreetMap", font, Brushes.Black, W-120, H-20);
        gis.sbp0.Text = " Zoom: " + Zoom;
        gis.sbp1.Text = " X: " + BX;
        gis.sbp2.Text = " Y: " + BY;
    }
}

class GIS : Form {
    public CheckBox cbCenter;
    public StatusBarPanel sbp0, sbp1, sbp2, sbp3;

    public GIS() {
        Text = "地図システム";
        this.ClientSize = new Size(800, 600);

        MenuStrip Menu = new MenuStrip(){ Dock = DockStyle.Top, ShowItemToolTips = true };
        MapPanel mapPanel = new MapPanel(this) { Dock = DockStyle.Fill };
        Panel ctrlPanel = new Panel() { Dock = DockStyle.Bottom, Height = 25 };
        this.Controls.AddRange(new Control[] { mapPanel, Menu, ctrlPanel });

        StatusBar statusBar = new StatusBar(){ Dock = DockStyle.Fill, ShowPanels = true };
        ctrlPanel.Font = new Font("Thoma", 10);
        sbp0 = new StatusBarPanel() { Width = 70 };
        sbp1 = new StatusBarPanel() { Width = 70 };
        sbp2 = new StatusBarPanel() { Width = 70 };
        sbp3 = new StatusBarPanel() { AutoSize = StatusBarPanelAutoSize.Spring };
        statusBar.Panels.AddRange(new StatusBarPanel[]{ sbp0, sbp1, sbp2 });

        cbCenter = new CheckBox() { Text = "中心点", Width = 70, Dock = DockStyle.Left };
        cbCenter.CheckedChanged += (s,e) => { mapPanel.Refresh(); };

        ctrlPanel.Controls.AddRange(new Control[]{ statusBar, cbCenter });
    }
}

class Program {
    [STAThread]
    static void Main() {
        Application.Run(new GIS());
    }
}

3.1 X,Y座標値の範囲

X, Y の値の範囲は 0 〜 2zoom であるから、スクロールおよび拡大・縮小では 表示の中心座標 (CX, CY) の変更に制約を受ける。

日本地図を表示しているとき、座標値が上の範囲を超えることはあり得ないが、 zoomが小さいとき(世界地図を表示しているとき)配慮がいる。

取り敢えずは、printMapメソッドを次のように修正した。 zoomが小さいとき、上下左右に地図が表示されない部分が生じることがあるが、 スクロールできるため、実用上は支障ない。

        int MAX = (int)Math.Pow(2,Zoom) - 1;
        int BX = Math.Max(0, (CX - W/2)/256);              // 切り捨て
        int EX = Math.Min(MAX,(CX + W/2 + 255)/256);        // 切り上げ
        int BY = Math.Max(0, (CY - H/2)/256);
        int EY = Math.Min(MAX,(CY + H/2 + 255)/256);