- Mouse Clicks and Cursor Position:
When you create a draw widget, you can enable mouse events (see Widgets).
If mouse events are enabled, you can detect which button was pressed/released.
First, you'll want to check if the detected widget event occured in the
draw widget. You can do this by checking the uvalue of the widget that
generated the event:
widget_control,ev.id,get_uvalue=uval
if uval eq 'draw' then begin...
Mouse events in draw widgets have properties press and release that receive
a value of 1 if it was the left mouse button, 2 for the center button, and
4 for the right button:
if ev.press eq 1 then print,'You clicked the left mouse button.'
if ev.release eq 4 then print,'You released the right mouse button.'
Now you might want to know the location of the mouse pointer when the
press or release occurred. This can be obtained by the cursor command:
cursor,x,y,/nowait,/data
x and y are variables that will receive the x and y coordinates of the
mouse pointer. /nowait specifies that the coordinates should be read
immediately, and you can specify /data for data coordinates or /normal
for normalized coordinates (just like in XYOUTS).
- System Variables:
Most system properties can be set through system variables. You have actually
used a system variable before: !P.multi. !P is a structure that is a system
variable. Structures are datatypes that contain multiple items, like arrays,
except that they can hold different datatypes within them, and the individual
items can have IDs that reference them. For instance !P could look like:
{ 0 [0,0,1,1] [0,1,1,0,0] } with the third item being the !P.multi array.
Since IDs can be assigned to each item though, the structure could maybe be
better thought of as looking like { window:0 position:[0,0,1,1]
multi:[0,1,1,0,0] }. Because of this, you don't need to know that multi is
the third item in !P. You can reference it by its id: !P.multi.
Other than !P, there is also !X and !Y (and !Z) which contain most of the
properties of the x and y (and z) axes. For instance setting !X.ticks=5
is the same as setting xticks=5 in a call to the PLOT procedure. Similarly
you could set !Y.margin=[2,4]. !D controls the display. All system variables
can be looked up in ? and can be very useful in more advanced programs.
- Designing programs:
A huge part of any program should be done before the first line of code is
entered. Whether its very organized and neatly mapped out, or as I usually
do, mostly think it out in my head and jot down a few notes about variables,
methods, and some snippets of code and pseudo-code, the following things
should be planned out before you begin programming: algorithms, methods,
quantities that need to be represented by variables or arrays, the
preferred data structures, certain loops and conditionals related to the
algorithms, possible errors, time complexity, and program control.
- Data Structures:
I really haven't talked much about data structures in IDL because it is not
an object oriented language. Usually arrays are the best choice for IDL.
But that is not always the case. Sometimes structures would be better. And
in other languages you may encounter structures such as stacks, queues,
linked lists, and trees. You have to determine which data stucture would
be best for your program. Stacks only allow you to push a value onto the
top of the stack and pop the top value off of the stack. Queues allow you
to insert data only at the back of the queue and remove data only at the
front of the queue so that the data remains in first in first out order
(FIFO queues are used when you burn CDs for instance). Linked lists
contain a reference to the memory location of the next object in the
sequence. Trees are traversable data structures where each object (or node)
can have several subnodes. They can be very efficient but are the most
difficult of the four to set up. Here is an example of a stack, but I
won't go more into it because this isn't a java class:
class Stack {
private int[] numbers;
private int n;
public Stack() {
this.numbers=new int[100];
this.n=0;
}
public void push(int x) {
numbers[n]=x;
n++;
}
public int pop() {
int x = numbers[n-1];
n--;
return x;
}
}This stack can be initialized by: Stack s = new Stack(); Then numbers
can be added by: s.push(5); or removed by x=s.pop(); Obviously, an error
will be generated if I try to add 101 numbers or remove a number from an
empty stack, but I wanted to keep this simple. If I were really writing a
stack, I would include error handling routines that would notify the user
if they tried to do a pop on an empty stack, but not cause an error, and one
that would increase the size of the stack on the fly if a user does a push
on a full stack. Anyone that has used the HP48 calculators should be familiar
with a stack. This is also a good example of how object oriented programming
differs from sequential programming. It can be much more flexible, but
requires a different mindset to write programs.
- Planning:
Before you start your program, the first thing you need to do is develop
your algorithm(s). You need to figure out how to solve the problem. It
will generally be an existing algorithm, but could be a new one. For instance
if your program was to sort objects by their RA and Dec, you may want to use
the Bubble Sort as your algorithm. Or maybe you need to design your own
custom loop. When designing a loop, you want to figure out the
post-condition first: what must be true when the loop ends. Then you need
to figure out a conditional that when falsified will cause the post-condition
to happen and an invariant that will remain constant throughout the loop.
Lastly, you have to make sure that you have a variant that will at some point
cause the conditional to become false so you don't get trapped in an infinite
loop. Once you decide on how you are going to
approach the program, then you can start planning how you are going to
implement it. Now you need to decide what type of data structures you are
going to use and what quantities need to be represented by variables.
You'll want to have FLTARRs to contain the RA and Dec (or maybe a 2-D
FLTARR) and a STRARR for the object names. Now is the pseudo-code portion
of planning. It is very helpful to write out in pseudo-code what you want
the program to do:
for j=0, # of objects-1 do begin
for l=j+1, # of objects do begin
if RA[j] gt RA[l] then swap RAs, Decs, names
endfor
endforNow that you have this outline, you will know what other variables
you are going to need, and you'll know the time complexity. In this case,
you will need a variable for the # of objects, and the time complexity is
order n^2: the time it takes to sort will be proportional to the square of
the number of objects. By using a winner tree, you could improve this to
order n*log(n), so you need to make a decision: if you are going to be handling
smaller quantities of data, then stay with the easy-to-write bubble sort.
However, if you are going to run the program many times on large quantities
of data, it may be more efficient to spend the time in developing the code
for a quick sort by using a winner tree because it will save a lot of time
during execution.
- Methods:
Methods are very important when designing programs. Whenever you are
going to use a portion of code multiple times, you want to put it in
a method that can be reuseable without rewriting any code. It is also
much easier to write error-free programs if you break big tasks down into
a series of much smaller, cleaner ones. In the example above, what if you
want to sort at multiple places in the program, and maybe by Dec or name
as well as RA? You could make your Bubble Sort routine into its own method:
pro sortData, x, y, z
for j=0,n_elements(x)-2 do begin
for l=j+1,n_elements(x)-1 do begin
if x[j] gt x[l] then swap x's, y's, z's
endfor
endfor
end
Now you could call this routine by: sortData,RA,Dec,name if you want to
sort by RA or sortData,name,RA,Dec if you want to sort by name (or of
course sortData,Dec,RA,name). It is called with 3 arrays and it sorts
based on the first one. Once you get this method working, you know it works
and you don't have to worry about anything anymore (see also push and pop
from the Stack example above). So there is no need for you to rewrite
any of the code and if something isn't working, you know it is in the
code that calls the method, not the method itself.
- Error Handling:
Now that you have started writing your methods, you need to think of
error handling. Try to think of anything that could go wrong (see the
Stack example above) and provide an error handling routine to deal with
each possible scenario. For instance in HW3, there is an option for
deleting a record. When I write the code for deleting the record, I write
it inside an IF block that checks to see if the requested record exists.
If it doesn't, the ELSE code is executed which prints a message that the
record was not found. As with the program, start small, and figure out
anything that could possibly go wrong in a method. Once the method is
able to handle any possible scenario, move onto the code that calls the
method.
- Putting it Together:
Once you have your program all planned out, then you can start writing it.
The planning could have taken weeks of figuring out algorithms and writing
pseudo-code, or seconds to think in your head that a Bubble Sort would
be the best way to do something. But now you're ready to write the program.
Start writing method by method. Don't try to write the whole thing top-down.
Of course it is easier to do this in object oriented programming, which is
by nature non-linear. I would write the class Stack, then write the code
to use the Stack. In IDL it works the same way though. In HW5, I wrote
the factorial function and interpolation procedures. Then I wrote the code
that printed out the factorials of 1-10 and the code that read in a file,
created a contour plot, interpolated, and created another contour plot. At
that point, I knew the interpolation procedure would work, so I didn't even
have to worry about it any more and could treat it the same as I would a
built-in command.
- Program Control:
Program control is about what method has control of the program when and
for how long and what it does with it. It is much more applicable to
object oriented programming of course, and in particular concurrent programming
when you may have multiple threads of control trying to access the same
things at the same time (in which case synchronization is required so that
you don't get a big mess). It is still a good idea to keep in mind in IDL
though. You also want to keep in mind any future additions you may want
to make. For instance, GatorPlot started out as just a tool to create
stacked plots of multiple spectra without having to rewrite code each time
I wanted to do something a little differently. Because of this, after v1.0,
I had to do a major redesign in the way a lot of things were handled. Now,
if I had to redo it from scratch, I would handle some things differently
to make it cleaner (especially regarding exporting files where the code
can get a little messy), but for the most part, it is very easy to add almost
any new features I want...it was very easy to add color and fitting for
instance. I have a procedure readdata that takes a lot of paramaters. I
just call this procedure and the data from all files is read into the
correct variables. Similarly, the makeplot procedure make the plot. I can
call this after setting the device to .PS and the plot is sent to a PS file
or I can call it when someone clicks Integrate to draw the plot to the screen
and allow the user to choose which part to integrate over. When someone
clicks Smooth, the smoothplot procedure is called. Immediately after that,
makeplot is called, which plots the now smoothed data. If I wanted to add
some feature that would manipulate the data in a new way and plot it, I would
simply have to write a procedure that would do the new data manipulation.
Then I could call readdata then the new procedure then makeplot. So it is
a good idea to break tasks down into smaller methods, and keep track of the
control flow of your program.