Grafikk
Pointing
Børre Stenseth
Moduler>Grafikk>Norgeskart

Norgeskart

Hva
Image1
Et enkelt eksempel på interaktiv grafikk

En enkel framstilling av et norgeskart basert på kommunepolygoner. Alle data er lagret som ressurser i programmet. Datastrukturen er beskrevet i modulen Noen datasett. Datagrunnlaget er:

  • fylker.txt som inneholder en liste med fylkesnavn og fylkesnummer
  • kindex.txt som inneholder en list med kommennr, kommunenavn og polygonhjørneindekser.
  • pnts.txt som inneholder alle polygonhjørner.

Dataene anses som faste og er lagret som ressurser til applikasjonen. Det er derfor ikke laget noen formatkontroll. Merk at dataene ikke nødvendigvis er riktige i den forstand at de er ajour med eventuelle kommunenavn og kommunesammenslåinger eller at det ikke er "hull" i Norge.

Programmet demonstrerer skalering av grafikk og mapping fra "norgeskoordinater" til skjermkoordinater og mapping tilbake av klikkpunkter for å se hvilken kommune vi har truffet.

Programmet har ingen annen funksjon enn å demonstrere et grafisk grensesnitt basert på kart. Det skal relativt lite til å kople dette grensesnittet til f.eks. databaseoppslag som henter data fra valgte kommuner eller fylker.

Programmet er organisert slik at Form1 tar seg av dialogen og holder lister av nødvendige data. Det er laget en egen klasse GeoUnit for å representere hver kommune.

Form

public partial class Form1 : Form
{
    // transformation matrix, set on draw and used on click
    Matrix TM;
    // all polygon points
    List<Point> pntList = new List<Point>(3200);
    // all kommunes, as Kommune-instances
    List<Kommune> kommuner = new List<Kommune>(450);
    // list of fylke (and country=0)
    fylke[] fylkeList;
    // selected fylke (or Norge=0)
    int selectedFylke = 0;
    protected String hilitedName = "";
    protected int hilitedIx = -1;
    protected String clickFoundName = "";
    protected String clickFoundId = "";
    protected int clickFoundIx=-1;
    public Form1()
    {
        InitializeComponent();
        InitializeMap();
    }
    protected void InitializeMap()
    {
        // load necessary data and prepare the datastructure
        // fill up pntList from data in properties
        // and find surrounding rectangle
        String t = Properties.Resources.pnts;
        String[] list = t.Split('\n');
        int MinX = int.MaxValue;
        int MaxX = int.MinValue;
        int MinY = int.MaxValue;
        int MaxY = int.MinValue;
        for (int ix = 0; ix < list.Length; ix++)
        {
            if(list[ix].IndexOf(',')==-1)
                continue;
            Point p = 
                new Point(Convert.ToInt32(list[ix].Split(',')[0]),
                          Convert.ToInt32(list[ix].Split(',')[1]));
            if (p.X < MinX) MinX = p.X;
            if (p.X > MaxX) MaxX = p.X;
            if (p.Y < MinY) MinY = p.Y;
            if (p.Y > MaxY) MaxY = p.Y;
            pntList.Add(p);
        }
        fylkeList = new fylke[21];
        // norge as fylke 0
        fylke fy = new fylke();
        fy.id = "00";
        fy.name = "Norge";
        fy.border = 
            new Rectangle(MinX, MinY, MaxX - MinX, MaxY - MinY);
        fylkeList[0]=fy;
        
        // make kommune index
        t = Properties.Resources.kindex;
        list = t.Split('\n');
        for (int ix = 0; ix < list.Length; ix++)
            if (list[ix].Trim().Length > 1)
                kommuner.Add(new Kommune(list[ix],pntList));
        // set up fylker (assume resource fylker sorted)
        String fs = Properties.Resources.fylker;
        String[] flist = fs.Split('\n');
        for (int fix = 1; fix < 21; fix++)
        {
            if (flist[fix-1].IndexOf(':') == -1)
                continue;
            fy = new fylke();
            String s = Convert.ToString(fix);
            if (s.Length == 1)
                s = "0" + s;
            fy.id = s;
            fy.name = flist[fix-1].Split(':')[1].Trim();
            if (fix == 13)// does not exist (was Bergen ?)
                fy.border = new Rectangle(1, 1, 1, 1);
            else
                fy.border = MakeBorder(fy.id);
            fylkeList[fix]=fy;
        }
        
        // fill combobox for fylke (and country) selection
        for (int cix = 0; cix < fylkeList.Length; cix++)
        {
            if(cix==13)
                continue;
            comboBoxView.Items.Add(fylkeList[cix].name);
        }
        comboBoxView.SelectedIndex = 0;
    }
    // prepare border for a fylker
    protected Rectangle MakeBorder(String id)
    {
        // run kommuneliste and identify borders of actual fylke
        int MinX=int.MaxValue;
        int MaxX=int.MinValue;
        int MinY=int.MaxValue;
        int MaxY=int.MinValue;
        for (int kix = 0; kix < kommuner.Count; kix++)
        {
            Kommune kom = (Kommune)kommuner[kix];
            String kid = kom.Id;
            // is it in correct fylke ?
            if (kid.Substring(0, 2).CompareTo(id)==0)
            {
                Rectangle kR = kom.Border;
                MinX = Math.Min(MinX, kR.Left);
                MinY = Math.Min(MinY, kR.Top);
                MaxX = Math.Max(MaxX, kR.Right);
                MaxY = Math.Max(MaxY, kR.Bottom);
            }
        }
        return new Rectangle(MinX,MinY,MaxX-MinX,MaxY-MinY);
    }
    // draw everything
    public void DrawMap(Graphics g, Rectangle R,Rectangle modelR)
    {
        // do necessary transformation
        g.ResetTransform();
        g.TranslateTransform(-modelR.Left, 
                             modelR.Bottom, 
                             MatrixOrder.Append);
        g.ScaleTransform(1.0f, -1.0f, MatrixOrder.Prepend);
        float scalefactor = 
            0.99f * Math.Min((float)R.Height / 
            (float)modelR.Height,
            (float)R.Width / (float)modelR.Width);
        g.ScaleTransform(scalefactor,
            scalefactor, 
            MatrixOrder.Append);
        // remember matrix
        TM = g.Transform;
        
        //transform is set, we draw all kommuner
        for (int ix = 0; ix < kommuner.Count; ix++)
        {
            List<Point> plist = kommuner[ix].Index;
            Point[] polygon = plist.ToArray();
            String kName=kommuner[ix].Name;
            String kId = kommuner[ix].Id;
            if (kName.CompareTo(hilitedName) == 0)
                g.FillPolygon(new SolidBrush(Color.Blue), polygon);
            else if (kName.CompareTo(clickFoundName) == 0)
                g.FillPolygon(new SolidBrush(Color.Red), polygon);
            else if ((kId.Substring(0,2).
                      CompareTo(fylkeList[selectedFylke].id)==0)
                     || (selectedFylke==0))
                g.FillPolygon(new SolidBrush(Color.White), polygon);
            g.DrawPolygon(new Pen(Color.Black), polygon);
        }
    }
    // where have we clicked
    public Boolean FindWhere(int x, int y, Rectangle R,Rectangle modelR)
    {
        // must translate the clickpnt to model coord
        Matrix m = TM.Clone();
        m.Invert();
        Point[] pts = new Point[1] { new Point(x, y) };
        m.TransformPoints(pts);
        int testCount = 0;
        clickFoundName = "None";
        while (testCount++ < kommuner.Count+1)
        for (int kix=0;kix<kommuner.Count;kix++)
        {

            if (kommuner[kix].GetHit(pts[0].X, pts[0].Y))
            {
                clickFoundName = kommuner[kix].Name;
                clickFoundId = kommuner[kix].Id;
                clickFoundIx = kix;
                return true;
            }
        }
        // not hit any kommune
        clickFoundIx = -1;
        return false;
    }
    private void panelMap_Paint(object sender, PaintEventArgs e)
    {
        // paint map
        DrawMap(e.Graphics, panelMap.Bounds, 
               fylkeList[selectedFylke].border);
    }
    private void InvalidateKommune(int kix)
    {
        if (kix != -1)
        {
            Rectangle r = kommuner[kix].Border;
            Point[] pts = 
                new Point[2] { 
                    new Point(r.Left, r.Top), 
                    new Point(r.Right, r.Bottom) };
            TM.TransformPoints(pts);
            panelMap.Invalidate(new Rectangle(pts[0], 
                new Size(pts[1].X - pts[0].X, pts[1].Y - pts[0].Y)));
            
        }
    }
    // click on map
    private void panelMap_MouseClick(object sender, MouseEventArgs e)
    {
        // clicked on the map
        // invalidate the one hilited now
        InvalidateKommune(clickFoundIx);
        if (FindWhere(e.X, e.Y, panelMap.Bounds, 
            fylkeList[selectedFylke].border))
        {
            labelName.Text = clickFoundId + " : " + clickFoundName;
            Rectangle r = new Rectangle(e.X - 100, e.Y - 100, 200, 200);
            // invalidate the new hilite
            InvalidateKommune(clickFoundIx);
        }            
    }
    // click on select button
    private void buttonFind_Click(object sender, EventArgs e)
    {
        // clicked the show button
        InvalidateKommune(hilitedIx);
        String who = textBoxLook.Text;
        hilitedIx = -1;
        if ((who != null)&&(who.Length>1))
        {
            //make start with uppercase               
            hilitedName = who.Substring(0, 1).ToUpper() + who.Substring(1);
            hilitedIx = FindKommuneIxByName(who);
        }
        InvalidateKommune(hilitedIx);
    }
    private int FindKommuneIxByName(string kname)
    {
        kname = kname.ToUpper();
        for (int ix = 0; ix < kommuner.Count; ix++)
        {
            string n = kommuner[ix].Name.ToUpper();
            if (n.CompareTo(kname) == 0)
                return ix;
        }
        return -1;
    }
    //change fylke (or Norway)
    private void comboBoxView_SelectedIndexChanged(object sender, 
                                                  EventArgs e)
    {
        
        String name = comboBoxView.SelectedItem.ToString();
        for(int ix=0;ix<fylkeList.Length;ix++)
            if(name.CompareTo(fylkeList[ix].name)==0)
            {
                selectedFylke = ix;
                // invalidate everything
                panelMap.Invalidate();
            }
    }
    private void panelMap_Resize(object sender, EventArgs e)
    {
        panelMap.Invalidate();
    }

Fylker og Norge er beskrevet ved en struct:

struct fylke
{
    public String name;
    public String id;
    public Rectangle border;
}

Kommune

class Kommune
{
    protected String name;  //Halden
    protected String id;    //0101
    List<Point> polypnts;
    protected Rectangle border;
    protected GraphicsPath grapath;
    
    public Kommune(String line,List<Point> pntList)
    {
        // line is kid:name:pointix,...
        id = line.Split(':')[0];
        if (id.Length == 3)
            id = "0" + id;
        name=line.Split(':')[1];
        String[] ilist = line.Split(':')[2].Split(',');
        polypnts = new List<Point>(ilist.Length);
        int maxx = Int32.MinValue;
        int minx = Int32.MaxValue;
        int maxy = Int32.MinValue;
        int miny = Int32.MaxValue;
        for (int ix = 0; ix < ilist.Length; ix++)
        {
            int v=Convert.ToInt32(ilist[ix]);
            Point p = (Point)pntList[v];
            maxx = Math.Max(p.X,maxx);
            maxy = Math.Max(p.Y,maxy);
            minx = Math.Min(p.X, minx);
            miny = Math.Min(p.Y, miny);
            polypnts.Add(p);
        }
        border = 
            new Rectangle(minx, miny, maxx - minx, maxy - miny);
        grapath = new GraphicsPath();
        Point[] Pt = (Point[])polypnts.ToArray();
        grapath.AddPolygon(Pt);
    }
    public List<Point> Index { get { return polypnts; } }
    public Rectangle Border { get { return border; } }
    public String Name { get { return name; } }
    public string Id { get { return id; } }

Mekanismen for å sjekke om vi har pekt på en kommune kan lages på mange måter. Vi kan enkelt teste på om et punkt er inne i et rektangel med metoden R.contains(p.X,p.Y), der R er et rektangel og p er et punkt. Videre er det en del rutiner som kan sjekke overlapping av regioner i et grafisk område. Det som er valgt er en allmen, håndskrevet, algoritme for å sjekke om et punkt er inne i et vilkårlig, lukket polygon. Implementasjonen ligger i Geounit:

public Boolean GetHit(int x, int y)
{
    if (!border.Contains(x, y))
        return false;
    return InsidePolygon(x, y);
}
// handmade inside test for polygon
private bool InsidePolygon(int x, int y)
{
    // Returns TRUE if p is inside pol
    // pol can be convex or concave
    // result is not interpretable if pol has crossing lines
    if (polypnts.Count < 3)
        return false;
    // Count intersections to the left of p
    // odd is hit, even is miss
    int pix;
    Point p1 = new Point();
    Point p2 = new Point();
    Point ps = new Point();
    bool Inside = false;
    // close the polygon
    polypnts.Add(polypnts[0]);
    p1.X = border.Left - 10;  // smaller than the smallest
    p1.Y = y;
    p2.X = border.Right + 10;  // bigger than the biggest
    p2.Y = y;
    for (pix = 0; pix < polypnts.Count - 1; pix++)
        if (Intersection(p1, p2, (Point)polypnts[pix], 
                        (Point)polypnts[pix + 1], ref ps))
        {
            if (ps.X < x) Inside = !Inside;
        }
    // unclose the polygon
    polypnts.RemoveAt(polypnts.Count - 2);
    return Inside;
}

private bool Intersection(Point p1, Point p2,
                          Point p3, Point p4,
                          ref Point ps)
{
    // finds the intersecton between lines p1-p2 and p3-p4
    // return TRUE if intersection, FALSE else
    // result in ps, if intersection
    int dx1 = p2.X - p1.X;
    int dx2 = p4.X - p3.X;
    int dy1 = p2.Y - p1.Y;
    int dy2 = p4.Y - p3.Y;
    int n = dx2 * dy1 - dy2 * dx1;
    if (n == 0)
        return false;
    double s = 
        1.0 * (dx1 * (p3.Y - p1.Y) - dy1 * 
        (p3.X - p1.X)) / (1.0 * n);
    if ((s < 0.0) || (s > 1.0))
        return false;
    double t = 
        1.0 * (dx2 * (p3.Y - p1.Y) - dy2 * 
        (p3.X - p1.X)) / (1.0 * n);
    if ((t < 0.0) || (t > 1.0))
        return false;
    ps.X = (int)(dx1 * t + p1.X);
    ps.Y = (int)(dy1 * t + p1.Y);
    return true;
}

Ut på tur

Prosjektet er bearbeidet litt til en variant der vi kan markere to kommuner og forsøke å finne en vei mellom dem. "Veinettet" er basert på at det går en vei mellom sentrum i to nabokommuner og har altså ikke noe med det reelle verinettrt å gjøre.

Programmet illustrerer en enkel variant av en søkealgoritme.

Referanser
Prosjekt:
https://svn.hiof.no/svn/psource/Csharpspikes/norge
Turprosjekt:
https://svn.hiof.no/svn/psource/Csharpspikes/norgepath
Vedlikehold

B.Stenseth, revidert februar 2011

(Velkommen) Moduler>Grafikk>Norgeskart (3D-OpenGL)