The Locale Explorer: GetGeoInfo
(and the use of mapping modes)

Home
Back To Tips Page

Back to LocaleExplorer

In addition to illustrating some of the use of GetGeoInfo, this page also shows how to use mapping modes. The map was created on a Web site that specializes in mapping software (see below) and is linear in both x and y, and I plot the coordinates directly on it, using mapping modes to normalize the range from x=-180..180 and y=-90..90.

Displaying the Map

To simplify the display of information, I simply converted the floating point latitude and longitude values to integers and plotted the "crosshairs" shown by plotting vertical lines from <x=longitude, y=-90..y=+90> and <x=-180..x=+180, y=latitude>.  To do this, I had to set up the coordinates on the map so this did the right thing.

Again, note that the map is a "linear coordinate" map, that is, unlike a Mercator projection, which distorts latitude and longitude (for example, in a Mercator projection, Greenland appears to be significantly larger than South America), this map creates a different type of distortion.  Discussion geographic distortions is well outside the scope of this essay.

To represent the map static, I created a new class

class CGeoMap : public CStatic

and added a number of methods.  The control uses a bitmap map generated by the Website http://www.aquarius.geomar.de/omc/make_map.html.This site also has information about the various kinds of mapping representations

The bitmap map vs. the logical map

I found it easier to deal with the drawing if I could avoid drawing on the bitmap.  So the CGeoMap creates a static window c_Overlay which is a transparent window.  I compute the coordinate lines on this transparent window.  Why do I do this?  Several reasons, and one is to decouple the actual map rectangle from the window in which it is displayed.  The generated bitmap has borders, and these borders contain decorative elements including the numbers defining latitude and longitude.  So how do I know where the actual map is?

I cheated.

I set this up so I could use different maps and I wanted to decouple the coordinates from a particular map.  However, for this particular map, I have a header file that defines its parameters:

// These coordinates were derived from the generated map by using
// Microsoft Paint (any other program that would give pixel coordinates
// for the mouse would work)

#define RECT_MAP1A CRect(55, 21, 485, 234) // -180, 90, 180, -90
// http://www.aquarius.geomar.de/omc/make_map.html
// Equidistant Cylindrical N90 W-180 E180 S-90

The comments explain it all.  By using Paint I simply counted pixels from each margin, did a little bit of arithmetic, and got this new CRect which represents the subset of the bitmap image that corresponds to the map.  I create the c_Overlay rectangle to fit inside the bitmap according to these specifications.

Why did I do this and what could I have done differently?

I did it because I do not allow this image to resize.  Whatever size it is, that's the size it becomes.  I adjust the size of the rectangle to fit the bitmap.  I could have scaled the bitmap, in which case I would have to account for the scaling factors in either x or y, or both.  I didn't feel this was necessary.

I did it because there is no way to compute the actual map size relative to the borders and other decorative effects.

I also did it because it was easy.  I had been recently at a Microsoft conference in Seattle, where we had wireless access from our hotel room.  I poked around for a while and got this nice map (well, I did have to go in and do a bit of cleanup of the digits to make them crisp).  A couple nights later, I was in a hotel down near Mt. St. Helens, where we didn't even have a phone in the room, and there was not even cell phone access (I had to call home via a pay phone outside the convenience store.  Remember pay phones?) and I wanted to get this code running, so I figured out how I could most elegantly live with a fixed-properties bitmap, and I came up with this solution.  Sometimes "good enough" works.

Setting the mapping mode

This is a great example of the use of MM_ANISOTROPIC mapping mode.  Although my accuracy was only to 1º of latitude or longitude, because the map shows only ±90º vertically, but ±180º horizontally, one degree has different actual offsets vertically and horizontally. So that is the natural choice.

To do this, I use SetWindowExt, SetViewportExt, SetViewportOrg and SetWindowOrg.  While I could have hardwired these computations, I wanted to decouple them from the actual map (so if I got another map, it would be easy to change them) and I also wanted to use variables that I could change with the Graphical Developer Interface features I added.  So I used variables, which I set in the SetMapRect function, which also allows me to decouple the map from its contents.

m_ViewportExt.cx = -r.Width() / 2;
m_ViewportExt.cy = -r.Height();
m_ViewportOrg.x = r.Width() / 2;
m_ViewportOrg.y = r.Height() / 2;
m_WindowExt.cx = -180;
m_WindowExt.cy = 180;
m_WindowOrg.x = 0;
m_WindowOrg.y = 0;

The rectangle r is the rectangle which is the client area of the overlay rectangle (the rectangle which is the client area of the transparent overlay window described above). 

Going around in circles

You would think it would be easy to draw a circle.  Well, it isn't, not in MM_ANISOTROPIC mode.  Take a look at my Viewport Explorer, where I allow the display of circles to help locate the origin.  In MM_ANISOTROPIC mode, these circles become ellipses with the aspect ratio of the mapping mode.  That is, if I were to draw a 20-unit circle with a mapping mode that was 2:1, I would get something that did not at all look like a circle.  The screen snapshot to the left, from the Viewport Explorer, shows what happens when an Ellipse operation is used drawing ellipses of increasing sizes, 100×100, 200×200, and so on, when the y axis is 50% of the x axis.  The result is not a circle.

Notice also that the width of the arrow line is also diminished; the horizontal arrow is half the thickness of the vertical arrow.  This can also be an unpleasant effect.

The solution to this is to convert the logical coordinates back into device coordinates, and do the drawing in device coordinates.  Then the line widths and the coordinates of the circle would be represented in device coordinates (MM_TEXT mode, or pixels).  Since pixels in modern displays are "square", that is, if we measure a vertical distance on the display and count the pixels, and measure a corresponding horizontal distance and also count the pixels, we get the same value for both directions. (The old EGA did not have square pixels, and was a real pain to program!)

The problem is that I don't actually draw on the overlay window; I draw in the OnPaint handler of my CGeoMap object.  This means that having computed the coordinates in logical coordinates of the overlay window, I have to convert them back to client coordinates (in MM_MAP mode, that is) in the actual CGeoMap window.  I do this in two steps.

First, I convert the logical coordinates (having set up the mapping mode as described above) to client coordinates of the overlay window that defines the actual boundaries of the map.

        CClientDC odc(&c_Overlay);
        Vertical.left = x;
        Vertical.right = x;
        Vertical.top = 90;
        Vertical.bottom = -90;
        odc.LPtoDP(&Vertical);

The LPtoDP conversion converts the logical coordinates represented by the "rectangle" to client coordinates.  But they are relative to the wrong window!

To convert coordinates in one window to coordinates in another window, we have to first convert them to a normalized set of coordinates.  This is easy: we use the screen coordinates!

        c_Overlay.ClientToScreen(&Vertical);
        ScreenToClient(&Vertical);

The ClientToScreen transformation converts the coordinates to screen coordinates.  These are useless to us.  But they allow us to transform the coordinates back to client coordinates in another window, the current window, by using ScreenToClient.  Note that the ClientToScreen is computed relative to the overlay window, but the ScreenToClient is computed relative to the current window.  This now gives me the coordinates I need to draw the lines (in this example, the vertical line) using unapped coordinates, and this means I won't get the scaling effects on the line size.

Finally, I can draw a circle, which because I'm using square pixels, means I can draw it within a simple square bounding box:  How big should the box be?

Well, I could choose a pixel value, but that leads to problems about display resolution.  Instead, I decided to draw a circle that was 15º on the map.  This normalizes its size.  But how big is 15º?  Well, that's easy:

        CSize c(15,15);
        odc.LPtoDP(&c)

The value 15 is in terms of degrees, but the LPtoDP converts it to pixels given the current mapping mode parameters!  Then I can use the value (I wanted it 15º in the vertical dimension of the map) to draw my circle:

    dc.Ellipse(Vertical.left - c.cy, 
               Horizontal.top - c.cy,
               Vertical.left + c.cy, 
               Horizontal.top + c.cy);

The result is the circle seen in the map, surrounding the point where the two lines cross.

The complete OnPaint handler

void CGeoMap::OnPaint() 
   {
    CPaintDC dc(this); // device context for painting

    HBITMAP b = GetBitmap();
    CBitmap * bmp = CBitmap::FromHandle(b);

    CDC memDC;
    memDC.CreateCompatibleDC(&dc);
    memDC.SelectObject(bmp);
    BITMAP info;
    bmp->GetBitmap(&info);
    dc.BitBlt(0,0, info.bmWidth, info.bmHeight, &memDC, 0, 0, SRCCOPY);

    //=============================================================================
    // -180                       0                         180
    // +--------------------------+--------------------------+ 90
    // |                          |                          |
    // |                          |                          |
    // |                          |                          |
    // +--------------------------+--------------------------+ 0
    // |                          |                          |
    // |                          |                          |
    // |                          |                          |
    // +--------------------------+--------------------------+ -90
    if(!MapRect.IsRectEmpty() && c_Overlay.GetSafeHwnd() != NULL)
       { /* draw lines */
        int save = dc.SaveDC();

        CClientDC odc(&c_Overlay);

        odc.SetMapMode(MM_ANISOTROPIC);
        odc.SetWindowExt(m_WindowExt.cx, m_WindowExt.cy);
        odc.SetViewportExt(m_ViewportExt.cx, m_ViewportExt.cy);
        odc.SetViewportOrg(m_ViewportOrg.x, m_ViewportOrg.y);
        odc.SetWindowOrg(m_WindowOrg.x, m_WindowOrg.y);
        int x = (int)Longitude;
        int y = (int)Latitude;

        //****************
        // Compute vertical
        //****************
        Vertical.left = x;
        Vertical.right = x;
        Vertical.top = 90;
        Vertical.bottom = -90;
        odc.LPtoDP(&Vertical);
        c_Overlay.ClientToScreen(&Vertical);
        ScreenToClient(&Vertical);

        CSize c(15,15);
        odc.LPtoDP(&c);

        //****************
        // Compute horizontal
        //****************
        Horizontal.left = -180;
        Horizontal.right = 180;
        Horizontal.top = y;
        Horizontal.bottom = y;
        odc.LPtoDP(&Horizontal);
        c_Overlay.ClientToScreen(&Horizontal);
        ScreenToClient(&Horizontal);

        //****************
        // Draw the lines
        //****************
        RegistryInt color(IDS_REGISTRY_MAPCOLOR);
        color.load(RGB(255,255,0));
        CPen pen(PS_SOLID, 0, color.value);
        dc.SelectObject(&pen);
        RegistryInt op(IDS_REGISTRY_MAPOP);
        op.load(R2_COPYPEN);
        dc.SetROP2(op.value);

        dc.MoveTo(Vertical.left, Vertical.top);
        dc.LineTo(Vertical.right, Vertical.bottom);

        dc.MoveTo(Horizontal.left, Horizontal.top);
        dc.LineTo(Horizontal.right, Horizontal.bottom);

        dc.SelectStockObject(HOLLOW_BRUSH);

        CRgn rgn;
        BOOL isrgn = rgn.CreateRectRgn(MapRect.left, MapRect.top, MapRect.right, MapRect.bottom);
        // int clip = dc.SelectClipRgn(&rgn, RGN_OR);
        // Note in the code below, cy appears for both x and y
        // This is because we want a circle in the device coordinates
        // Using cx gives a circle in transformed coordinates, which
        // comes out as an ellipse in device coordinates
        dc.Ellipse(Vertical.left - c.cy, Horizontal.top - c.cy,
        Vertical.left + c.cy, Horizontal.top + c.cy);
        dc.RestoreDC(save);
       } /* draw lines */
    //=============================================================================
    // Do not call CStatic::OnPaint() for painting messages
   }

Sorting the data

I use a CListCtrl to handle the display of raw geographic data.  But sorting a list control is tricky.

For example, the SortItems call takes two parameters: a pointer to a compare function, and a DWORD_PTR that is user-defined information passed to that function.  The function prototype that is called is

int CALLBACK procname(LPARAM lParam1, LPARAM lParam2, LPARAM lParamSort)

where lParamSort is the value passed to the SortItems. The value returned, like the qsort function, is less than 0 if lParam1 < lParam2, zero if lParam1 == lParam2, and greater than 0 if lParam1 > lParam2.  But how do we do a comparison?

First, when the user clicks on a column of the list control, it is necessary to load up the LPARAM value.  But what is the value?  And how do we compare floating point numbers, which don't fit well in an LPARAM?  And if we did fit them in, how do we tell an LPARAM which is holding a floating point number from one which is holding an integer from one which is holding a pointer to text?

I decided to simplify it by always storing a text pointer in the list control.LPARAM.  I also subclassed the control so I knew something about the column that was being clicked.

So what I did was, when a column was clicked, convert the data from the text stored in the column of the control to a canonical format, which was a pointer to a string.  But that isn't sufficient.  Textual sorting of digits gives you sequences like <1, 10, 102, 11, 110, 111, 19, 2, 20, 233, 3, ...>; that is, it sorts the digit strings alphabetically (think of the case if the digits were transformed to letters, where 0=A, 1=B, etc., then we would have <B, BA, BAC, BB, BBA, BBB, BI, C, CA, CDD, D, ...>, and so on).  So it is necessary to "normalize" the number, such as representing the value as 00001, 00010, 00102, 00011, 00110, 00111, 00019, 00002, 00020, 00233, 00003, which when sorted alphabetically would produce the sequence <00001, 000002, 000003, 00010, 00011, 00019, 00020, 00102, 00110, 00111, 00233>, so "alphabetic order" is also "numeric order".  This is a standard trick of doing numeric sorts.

The floating-point values complicate this a bit.  For reasons completely incomprehensible to anyone who knows science, the accuracy of some coordinates is to three places (e.g., the latitude of Paraguay is returned as the string -23.244), but some are returned to only two digits (New Caledonia is -21.30),  one decimal place (Senegal is given as 14.9), or no decimal places (Zimbabwe is given as -19, although Tokekau is given as -9.00).  This makes no sense.  For example, it implies that we don't actually know the location of Zimbabwe to within a degree, although we know the location of Paraguay to a thousandth of a degree! So it is necessary to normalize the value to a canonical number of decimal places.  So what I did was convert the value from text-to-double, and then formatted the double using a standard format for all values, such as %08.3f, which puts the desired leading zeroes and three decimal places.  So values would sort the values as -023.244, -021.300, -014.900, -019.000, or -009.000. 

This worked fine for the integer numeric values, which were all unsigned. But the problem with the negative numbers is that they sorted textually, not geographically.  The - meant all the negative values sorted ahead of all the (unsigned) positive values, but it also meant that the values sorted as <-001.000, -002.000, -075.000>, whereas you would expect them to sort from largest negative to smallest negative, that is, the expected sort is <-075.000, -002.000, -001.000>.  So alphabetical sorting isn't really going to work.

Fortunately, in this case, we know that the range of the values cannot exceed  ±180º, so what I did was bias the value by 180.  Thus, -180º became 000.000 and 180º became 360.000, so the alphabetical sorting now works again.

This also requires that we create a heap-allocated string object and point the LPARAM value of each element to it.  Now there is a problem: what to do with those values?  The answer is simple: right after sorting them, delete them and set the LPARAM value to NULL

void CGEOID::OnColumnclick(NMHDR* pNMHDR, LRESULT* pResult) 
   {
    NM_LISTVIEW* pNMListView = (NM_LISTVIEW*)pNMHDR;
    *pResult = 0;

    int column = pNMListView->iSubItem;

    LVCOLUMN item;
    CString text;

    BOOL floating = FALSE;
    BOOL numeric = TRUE;

    item.mask = LVCF_TEXT;
    item.pszText = text.GetBuffer(MAX_PATH);
    item.cchTextMax = MAX_PATH;
    GetColumn(column, &item);

    text.ReleaseBuffer();
    if(text == _T("GEO_LATITUDE") ||
       text == _T("GEO_LONGITUDE"))
          floating = TRUE;
    else
       { /* might be numeric */
        // See if sort is numeric or textual value
        for(int i = 0; i < GetItemCount(); i++)
           { /* scan column */
            CString s = GetItemText(i, column);
            CString t = s.SpanIncluding(_T("0123456789"));
            if(s != t)
               numeric = FALSE; 
           } /* scan column */
       } /* might be numeric */

    for(int i = 0; i < GetItemCount(); i++)
       { /* prepare for sorting */
        LPVOID p = (LPVOID)GetItemData(i);
        delete p;
        SetItemData(i, NULL);
       } /* prepare for sorting */

    for(i = 0; i < GetItemCount(); i++)
       { /* set column data */
        CString s = GetItemText(i, column);
        if(floating)
           { /* floating point */
            double d = _tstof(s);
            d += 180.0; // bias by 180°
            s.Format(_T("%08.3f"), d);
           } /* floating point */
       else
       if(numeric)
          { /* convert to canonical */
           CString t;
           t.Format(_T("%09d"), _ttoi(s));
           s = t;
          } /* convert to canonical */
       LPTSTR p = new TCHAR[s.GetLength() + 1];
       if(p != NULL)
          { /* can store it */
           lstrcpy(p, s); // safe because we know new buffer is long enough
           SetItemData(i, (LPARAM)p);
          } /* can store it */
       else
          ASSERT(FALSE); // allocation failure
       } /* set column data */

    SortItems(compare, NULL);
    TRACE0("====================================\n");
    for(i = 0; i < GetItemCount(); i++)
       { /* prepare for sorting */
        LPTSTR p = (LPTSTR)GetItemData(i);
        TRACE2("%d: %s\n", i, p);
        delete p;
        SetItemData(i, NULL);
       } /* prepare for sorting */

   }

Note that the sort-of-kludge is the fact that I know the caption of the columns which holds the floating-point data.  I don't use a STRINGTABLE entry because I'm using the actual name from windows.h, so it is nominally localization-independent.

You would think this code is straightforward.  To be Unicode-compliant, I use the tchar.h definitions for Unicode-independence, so I would like to use something like _ttof instead of atof.  But there isn't any _ttof function defined.  Consulting the VS.NET documentation, it was given the rather non-obvious name of _tstof, but this is not defined in VS6.  So I had to define _tstof for backward compatibility. 

The comparison operator is quite simple:

/* static */ int CALLBACK CGEOID::compare(LPARAM lParam1, LPARAM lParam2, LPARAM)
   {
    LPTSTR s1 = (LPTSTR)lParam1;
    LPTSTR s2 = (LPTSTR)lParam2;
    return lstrcmpi(s1, s2);
   } // CGEOID::compare

Floating-point Unicode conversion

I needed to build this under VS6.  In one of the more grotesque oversights I've encountered in a long time, there is no _wtof, function to convert a string of Unicode characters representing a floating point number to a double. Since I needed to compile this under VS6 as well as VS.NET, I had to define a version of _tstof, one that would work in Unicode and ANSI.

#ifndef _tstof
#ifdef _UNICODE
double _tstof(LPCTSTR v);
#else
#define _tstof atof
#endif // _UNICODE
#endif // _tstof

The code to implement this is a bit weird; what I do is use the W2A macro to convert the string to ANSI and apply atof.

#ifndef _tstof

#ifdef _UNICODE
#include "atlconv.h"
#endif // _UNICODE

#include "_tstof.h"
#ifdef _UNICODE
double _tstof(LPCTSTR v)
   {
    USES_CONVERSION;

    LPCSTR p = W2A(v);

    double d = atof(p);
    return d;
   } // _ttof
#endif // _UNICODE

#endif // _tstof

Using GetGeoInfo

I encapsulated the calls of GetGeoInfo in a series of functions.  By the time I got around to writing this particular module, I had already created other pages, such as the GetGeoInfo page, so I was able to use it to generate the functions I needed.

CString Get_GEO_FRIENDLYNAME(GEOID geoid, LANGID langid)
   {
    CString friendlyname_data;
    int length = ::GetGeoInfo(geoid, GEO_FRIENDLYNAME, NULL, 0, langid);
    if(length != 0)
       { /* has GEO_FRIENDLYNAME */
        LPTSTR p = friendlyname_data.GetBuffer(length);
        length = ::GetGeoInfo(geoid, GEO_FRIENDLYNAME, p, length, langid);
        friendlyname_data.ReleaseBuffer();
       } /* has GEO_FRIENDLYNAME */
    else
       { /* error GEO_FRIENDLYNAME */
        DWORD err = ::GetLastError();
        friendlyname_data = (err == ERROR_INVALID_PARAMETER ? _T("?") : ErrorString(err));
       } /* error GEO_FRIENDLYNAME */
    return friendlyname_data;
   } // Get_GEO_FRIENDLYNAME

Note the technique of converting the error code.  For certain locales, for some reason the kernel simply returns ERROR_INVALID_PARAMETER.  In this case, I chose to display a "?" to indicate the error.  But for other errors, I wanted to see the error message itself.

Using the Graphic Developer Interface

Of course, the first time I tried to set this code up, it didn't work.  When I wrote Win32 Programming, I carefully worked out the conversion formulae.  The problem with a book that thick is that I don't carry it around with me, particularly cross-country, so I didn't have it with me, and I find the Microsoft documentation a bit lacking.  So I spent about ten minutes trying to debug with breakpoints and writing things down.  But I didn't have a desk, and sitting in bed made it inconvenient to take notes.  So why bother?  I could add a few controls to the display, and I'd have a nice debugging environment.  I did this, and my debugging instantly speeded up.  When possible, debug in terms of the problem domain, not the source code domain.  So note that in the image above (captured from the release version) there are no controls under the List Control giving the coordinates, but in the debug version, the additional controls shown to the left appear.  By fiddling the values, I could see exactly what was going to happen for various settings.

It was shortly after doing this that I decided to create the Viewport Explorer.  It was a lot easier to carry around!

[Dividing Line Image]

The views expressed in these essays are those of the author, and in no way represent, nor are they endorsed by, Microsoft.

Send mail to newcomer@flounder.com with questions or comments about this web site.
Copyright © 2005=2006 Joseph M. Newcomer/FlounderCraft Ltd.  All Rights Reserved.
Last modified: May 14, 2011