Filed under MapInfo

Detecting key states in MapBasic

Here’s another neat little MapBasic function I’ve been using recently. I needed a way of detecting whether a key on the keyboard was currently pressed or not. My intention was to show a hidden debugging dialog if a user shift-clicked on a specific button control, but there’s no in-built functions in MapBasic for detecting key states. Fortunately it’s possible to hook into the Windows User32.dll to use the GetAsyncKeyState call. Here’s how this all works in MapBasic…

First, we’ve got to define the GetASyncKeyState call and a bunch of related constants which map keys to a numeric value. Copy and paste the contents below to a .def file (or download a pre-made version later in this post).

asynckeys.def:

Declare Function GetAsyncKeyState Lib "User32.dll" Alias "GetAsyncKeyState" (ByVal vKey As Integer) As Integer
DEFINE vbKeyShift 16
DEFINE vbKey1 49
DEFINE vbKey2 50
DEFINE vbKey3 51
DEFINE vbKey4 52
DEFINE vbKey5 53
DEFINE vbKey6 54
DEFINE vbKey7 55
DEFINE vbKey8 56
DEFINE vbKey9 57
DEFINE vbKey0 48
DEFINE vbKeyBack 8
DEFINE vbKeyTab 9
DEFINE vbKeyReturn 13
DEFINE vbKeyControl 17
DEFINE vbKeyMenu 18
DEFINE vbKeyPause 19
DEFINE vbKeyEscape 27
DEFINE vbKeySpace 32
DEFINE vbKeyEnd 35
DEFINE vbKeyHome 36
DEFINE vbKeyLeft 37
DEFINE vbKeyRight 39
DEFINE vbKeyUp 38
DEFINE vbKeyDown 40
DEFINE vbKeyInsert 45
DEFINE vbKeyDelete 46
DEFINE vbKeyMultiply 106
DEFINE vbKeyDivide 111
DEFINE vbKeyAdd 107
DEFINE vbKeySubtract 109
DEFINE vbKeyDecimal 110
DEFINE vbKeyF1 112
DEFINE vbKeyF2 113
DEFINE vbKeyF3 114
DEFINE vbKeyF4 115
DEFINE vbKeyF5 116
DEFINE vbKeyF6 117
DEFINE vbKeyF7 118
DEFINE vbKeyF8 119
DEFINE vbKeyF9 120
DEFINE vbKeyF10 121
DEFINE vbKeyF11 122
DEFINE vbKeyF12 123
DEFINE vbKeyNumlock 144
DEFINE vbKeyScrollLock 145
DEFINE vbKeySnapshot 44
DEFINE vbKeyPageUp 33
DEFINE vbKeyPageDown 34
DEFINE vbKeyNumpad1 97
DEFINE vbKeyNumpad2 98
DEFINE vbKeyNumpad3 99
DEFINE vbKeyNumpad4 100
DEFINE vbKeyNumpad5 101
DEFINE vbKeyNumpad6 102
DEFINE vbKeyNumpad7 103
DEFINE vbKeyNumpad8 104
DEFINE vbKeyNumpad9 105
DEFINE vbKeyNumpad0 96
DEFINE vbKeyPressed -32767
DEFINE vbKeyWasPressed 1

Now, you can test for the state of any key by calling getASyncKeyState( vKey ), where vKey is an integer value corresponding to the key you want to test. So getASyncKeyState( vbKeyControl ) will test the state of the control key. The value returned will reflect whether the key is currently pressed (vbKeyPressed), or has been pressed since the last call to the function (vbKeyWasPressed). Easy! Here’s a little sample program to demonstrate how this all works:

Include "asynckeys.def"
Declare Sub Main
Declare Sub KeyCheck

Sub KeyCheck
    ' Store the pressed state of the SHIFT key
    Dim iState As Integer
    iState = getASyncKeyState( vbKeyShift )

    ' Check what the state was
    Do Case iState
        Case vbKeyPressed
            Print "SHIFT is being pressed!"
        Case vbKeyWasPressed
            Print "SHIFT has been pressed since last check"
        Case Else
            Print "Nothing to report..."
    End Case

    ' Keep the dialog around
    Dialog Preserve
End Sub

Sub Main
    Dialog
        Title "ASync Key State"
        Control OKButton
            Title "Check"
            Calling KeyCheck
        Control CancelButton
            Title "Quit"
End Sub

Both the asynckeys.def definition file and the sample program are contained in this archive.

Tagged , , , , ,

Regular expressions in MapBasic

I’m going to take a break from all my recent QGIS posts to talk about some MapBasic news… I’m proud to announce the release of MbRegEx, an open-source library for using regular expressions in MapBasic scripts!

If you’re not familiar with regular expressions, they’re an extremely powerful tool for string manipulation. They can be somewhat daunting at first, but with a bit of practice they’ll open up all kinds of string processing which would otherwise be extremely convoluted or impossible.

Up until now there’s been no way of using the beauty of regular expressions within MapInfo. Now, with MbRegEx, all their mighty power can be fully utilised within your MapBasic scripts!

Included Functions

The library contains 5 functions for use in MapBasic scripts:

RegExTest(string, regex): returns true or false depending on whether string contains the specified regular expression. For example, RegExTest( “ABC123”, “[A-Z]{3}\d{3}” ) returns true, RegExTest( “AB12”, “[A-Z]{3}\d{3}” ) returns false. This function really comes in handy when selecting records, eg:

SELECT * FROM address WHERE RegExTest( street, "MAIN (ST|RD)( [NESW])?$")

to select all addresses where the street is either “MAIN ST” or “MAIN RD” and which may have an optional N/E/S/W suffix.

RegExMatch(string, regex): returns the first part of string which matches the specified regular expression. Great for quickly extracting specific parts of a string. Let’s say we’ve got an address field, and we’d like to grab just the street number from it. We could use the regular expression “^(\d+[A-Z]*)”, which will match any leading numbers with optional letter suffixes. So RegExMatch( “12A Main St”,  “^(\d+[A-Z]*)” ) returns “12A”,  whereas RegExMatch( “Upper Main St”,  “^(\d+[A-Z]*)”) will return an empty string. Trying to achieve this same task using built-in MapBasic functions alone would be extremely tedious and error-prone!

RegExMatchAll(string, regex, array) and RegExMatchMultiple(string, regex, array) will fill array with all the parts of string which pass the specified regular expression. RegExMatchAll is used when you have just one capturing group in your regular expression, and RegExMatchMultiple is used for multiple capturing groups.

This is probably best demonstrated with some examples:

RegExMatchAll( "12A Main St", "\b(\w+?)\b", sMatches )

will fill the sMatches array with all the individual words from “12A Main St”. So sMatches(1) = “12A”, sMatches(2) = “Main”, etc.

RegExMatchMultiple( "10.5.2", "(\d+)\.(\d+)\.(\d+)", sMatches )

fills sMatches with the results of the three capturing groups (the bracketed parts) of the regular expression. Thus sMatches(1) = “10”, sMatches(2) = “5” and sMatches(3) = “2”.

Again, these two functions are great for splitting a string into its component parts or doing something fancy like extracting all the phone numbers from a paragraph of text.

Lastly, RegExReplace(string, regex, replacement, destination) replaces the parts of string which match the regular expression with replacement, and stores the result in destination. One handy use for this is removing invalid characters from a string, such as

RegExReplace("T/:e@st!i~n&g#1,^2}3", "[^A-Za-z0-9]", "_", sDest)

This results in sDest = “T__e_st_i_n_g_1__2_3”.

Getting Started

There’s a few extra examples and full instructions for using these functions in the “test.mb” file included with MbRegEx. You can use these as a starting point for your own MapBasic scripts. If regular expressions are new to you, I recommend regular-expressions.info as a great resource together with an online regex visualiser such as debuggex.

Also, a quick warning – MbRegEx does no validation or testing of your regular expressions, so be careful with badly formatted expressions as they’ll cause MapInfo to crash. If in doubt, run the expression through an online tool to validate it first.

Downloads

The full source code is available on GitHub, under a public domain license. If you’re looking for the easiest way to get started, just download this zip which contains both the MbRegEx DLL and a sample MapBasic program demonstrating all the different regular expression functions which are available in the library.

End note – building the MbRegEx DLL file

I’ve included a CodeBlocks project file with the source, but building the MbRegEx.dll file can be difficult – you’ll need to first compile the boost libraries using mingw on your system. Then, make sure that in the CodeBlocks project build options, under Linker settings you have your “stage\lib\libboost_regex-mgw46-1_52.a” (or similar) correctly linked. Also, under Search directories add your boost folder to the Compiler tab. 

Tagged , , , ,

Investigating MapInfo’s Geocode Routine

After doing a bit of work using MapInfo’s built-in Geocode routine I started getting curious about how the routine handles various special cases. After a bit of experimenting I thought I’d document what I found out.

Starting with the simplest case, a street with odd numbers (1-9) on the left, and even numbers (2-10) on the right. The red stars show where MapInfo geocodes house numbers 3 and 4:


So far so good – we can see that MapInfo has correctly determined that one side of our street corresponds to odd numbers and one to even, and it has correctly matched the address points to the corresponding side of the road. Let’s try a one sided road next:

MapInfo treats -9999 values in an address ranges table as “no address points”. This road segment is correctly handled by MapInfo, and house number 4 gets geocoded to the correct side of the road. Nothing unexpected so far, but now let’s split the street into two lanes, with odd numbers on one side and even on the other:

More or less what we want to see. I should point out that in all these tests I’ve left the default setting of insetting addresses 15% from the ends of the street, which explains why points 2/10 and 1/9 don’t fall right on the beginning and end of the road segments. I’m not sure why the points curve out and fall at varying distances to the road — but it’s close enough and I can live with that.

Just to see what happens, lets go back to one road segment with valid even numbers (2-10) on only one side, and try geocoding an odd number house (9):

MapInfo has geocoded the point right in the centre of the road segment. Not what we’d like, but at least the Find command returns a result of 11 (exact match, side of street undetermined) for this case and it’s possible to find and avoid these types of errors. What happens now if we mix odd and even numbers on the same road side?

Here we’ve set one side of the road to range from 1 to 10, and the other to contain no addresses (-9999). Unfortunately none of the house numbers, either odd or even, match correctly in this case. All numbers from 1-10 are placed at the centroid, with a result code of 11. I guess mixing odd and even numbers in a street segment like this should be avoided.  If you have a street segment which does contain a range of odd and even numbers in reality, it looks like the only way to get this to work correctly is to duplicate the road segment, once with an odd number range and once with an even range. Hmmm… I wonder what happens if we have two matching street segments, one with this mix of odd and even numbers, and the second with just odd numbers?

Good – that’s encouraging to see. MapInfo can match the odd numbered houses to the segment with an odd address range of 1-9, even when we have a bad segment which covers the same number range. The even numbers don’t return a match, with a result code of -411 (multiple matches, side of street undetermined, exact match). Let’s take a step back and try a different type of conflict, where the address numbers on either side of the road overlap:

After a bit more testing (which I won’t go into here), it looks like when there’s an overlapping range like this and an address point could fall on either side of the road, MapInfo places it on the right hand side. For reference, overlapping ranges across two different road segments will geocode points which unambiguously fall on one road segment, and return a code of -401 (multiple matches, exact match) for the others:

(Address points 2 and 10 geocode, whereas 4, 6 and 8 don’t). One last thing to try – let’s see what happens when the address ranges table contains a point object. In this case we’ll add a point object with a left range of 1-9 and a right range of 2-10.

I wasn’t expecting this to work, but it appears to correctly geocode any numbers between 1 and 10 directly to the same location as the address ranges point. This is great news (for reasons I’ll possibly go into in a later blog post).

Quick Summary:

  • Don’t have an address range on the one side of a segment which consists of both odd and even numbers. If you do need to have a segment like this, you’ll need to duplicate the segment with one row having and odd range and the other having an even range.
  • Make sure to correctly handle any matches with a result code of side of street undetermined, since these may have just matched to the centre of the road
  • It’s OK to have point objects in an address ranges table
Tagged , , ,

Alpha Shapes for MapInfo

One recurring question which pops up on the MapInfo mailing list relates to creating a polygon which encloses a set of objects.  While this can sometimes be achieved with a Convex Hull, the results aren’t always that useful. For example, take a bunch of points in the shape of the letter A:

Using the standard Convex Hull creates a polygon which totally encloses all these points. However, the polygon doesn’t closely follow the visual shape of the points and covers a much larger area then one would expect:

Convex hull

Alpha shapes are an alternative solution to polygon wrapping. An alpha shape will wrap the input points in a different manner to convex hulls, which more closely follows the ‘visual’ outline created by the points:

Alpha Shape

Mathematically, the concept of alpha shapes is well defined and they can be easily created from subsets of the Delaunay triangulations of an input set. Unfortunately, MapInfo doesn’t include any built-in methods for creating Delaunay triangulations, but it does have a tool for creating Voronoi diagrams. Lucky for us it’s possible to transform a Voronoi diagram into Delaunay triangles.

Ok, now all that background information is out of the way, it’s time to announce mi-alphashapes, a MapBasic based tool for creating alpha shapes in MapInfo!

mi-alphashapes in action

To use, simply load the mbx file and choose Alpha Shapes from the menu. You’ll need to enter an appropriate alpha value – larger values will cause the resultant polygon to behave more like a Convex Hull, smaller values will wrap the points more tightly but may result in multiple shapes. While the extension automatically calculates a sensible default value, you may need to experiment with this to get the best results. This is easy under MapInfo 10.5+, since the extension includes a handy preview window. Additionally, the tool also includes a routine for creating Delaunay triangulations.

Warning: You may run into problems if you’re not using projected coordinates.

I’ve uploaded all the source to GitHub, under a Public Domain license. Feel free to do with it what you will. Otherwise, compiled versions for both MapInfo 8.5+ and MapInfo 10.5+ are available here.

Tagged , ,

Better Dialogs in MapBasic

Any easy way to add some extra polish to MapBasic applications is to switch from using the MapBasic Note and Ask commands to standard Windows message boxes. While the Note command is handy for quickly giving feedback to a user, there’s zero options for customising the dialogs.

Standard MapBasic “Note” dialog – limited to an exclamation icon and “MapInfo” title bar

Let’s see what we can do about that. We’ll start by including a reference to the standard Windows dialog, which is included in the User32.dll library. I also use two tiny wrapper sub/functions (called MessageBox and MsgBox respectively). The function is used for dialogs which need to return a response (a replacement for the Ask function), and the sub for when the response isn’t important (replacement for Note). Lastly, there’s also a bunch of DEFINEs to make calling the routines more convenient and memorable.

Declare Function MsgBoxA Lib "User32.dll" Alias "MessageBoxA" (ByVal hWnd As Integer, ByVal sTxt As String, ByVal sCaption As String, ByVal iTyp As Integer) As Integer
Declare Function MsgBox(ByVal sTxt As String, ByVal sCaption As String, ByVal iType As Integer) As Integer
Declare Sub MessageBox(ByVal sTxt As String, ByVal sCaption As String, ByVal iType As Integer)
' Messagebox dialog buttons
DEFINE vbOKOnly 0
DEFINE vbOKCancel 1
DEFINE vbAbortRetryIgnore 2
DEFINE vbYesNoCancel 3
DEFINE vbYesNo 4
DEFINE vbRetryCancel 5
' Messagebox dialog icons
DEFINE vbCritical 16
DEFINE vbQuestion 32
DEFINE vbExclamation 48
DEFINE vbInformation 64
' Messagebox default button
DEFINE vbDefaultButton1 0
DEFINE vbDefaultButton2 256
DEFINE vbDefaultButton3 512
DEFINE vbDefaultButton4 768
' MsgBox Returned value
DEFINE vbOK 1
DEFINE vbCancel 2
DEFINE vbAbort 3
DEFINE vbRetry 4
DEFINE vbIgnore 5
DEFINE vbYes 6
DEFINE vbNo 7
'**************************************************************************
' Wrapper for standard Win32 Msgbox function
'**************************************************************************
Function MsgBox(ByVal sTxt As String, ByVal sCaption As String, ByVal iType As Integer) As Integer
 MsgBox = MsgBoxA(WindowInfo(WIN_MAPINFO, WIN_INFO_WND), sTxt, sCaption, iType)
End Function
'**************************************************************************
' Messagebox which doesn't return a value
'**************************************************************************
Sub MessageBox(ByVal sTxt As String, ByVal sCaption As String, ByVal iType As Integer)
 Dim i As Integer
 i = MsgBoxA(WindowInfo(WIN_MAPINFO, WIN_INFO_WND), sTxt, sCaption, iType)
End Sub

Now that we’re all setup, we can duplicate the standard MapInfo message box with the call:

Call MessageBox("Standard Windows message box", "My MapBasic Program", vbExclamation)

Message box with customised title

As seen above, MessageBox is called by passing the text of the dialog, a title caption, and one or more options. In this case we’ve used vbExclamation to copy the appearance of the Note command. Nothing too special yet, but let’s explore a little further. In some cases (such as notifying the user when an operation has successfully completed) the exclamation icon just looks wrong. Compare:

Sometimes the exclamation icon isn’t the best choice…

To the dialog created by the code

Call MessageBox("Processing Complete!", "Friendlier Dialog", vbInformation)

Much friendlier!

Let’s take the other extreme, when you need to let the user know that something really bad happened:

Call MessageBox("Something really bad happened..!", "Error", vbCritical)

Guaranteed to get attention!

The other way to use the Message Box is through the MsgBox function. This can be used to ask the user for a response, in a similar way to MapBasic’s Ask function. The big difference is that MapBasic’s Ask dialog causes your eyes to bleed:

MapBasic’s Ask dialog… what’s with all the empty space?

Let’s replace this with a standard Windows message box:

iResponse = MsgBox("Would you like to continue?", "Messagebox with two buttons", vbYesNo + vbQuestion + vbDefaultButton1)
If iResponse = vbYes Then
  ' User clicked yes
ElseIf iResponse = vbNo Then
  ' User clicked no
End If

The code above demonstrates how dialog attributes can be combined:

vbYesNo + vbQuestion + vbDefaultButton1

and how the value returned by the dialog can be checked against the vbYes and vbNo constants to determine the user’s response. Chaining options together allows for very flexible, user-friendly dialogs.  Again, the result looks much nicer (and more professional) then the built-in MapBasic function:

Using a Message Box to ask a question

So there you go! A simple little change you can make to your MapBasic programs to make them just a bit more user-friendly.

Tagged ,