15-212-X : Homework Assignment 4
Due Wed Oct 22, 2:00pm (electronically)
Maximum Points: 100 (+20 extra credit)
There was a bug in the version of this assignment handed out in class, which has been corrected below. To the see correction only, check the file correction.txt.
In this assignment, we develop a data structure called a region that plays a fundamental role in the implementation of a graphical windowing system.
A graphical windowing system divides the screen into areas where applications can draw their output. Because windows can overlap, the windowing system must allow drawing within arbitrarily shaped parts of the screen. Windowing systems use regions to represent the visible part of a window.
You can think of the screen as grid of small rectangles (usually more than a million per screen) called pixels. Each pixel is defined by its coordinate location in the grid, and its color. To draw an object on the screen, you specify which pixels to change, and how to color those pixels. A region only describes the set of pixels to change, and does not describe their color.
The version of regions that you will implement is similar to the regions in the windowing system on the Macintosh. Bill Atkinson developed the first implementation of regions for the Macintosh around 1982. Parts of the code were later patented. Apple has never published the region format, but this has not stopped several large application developers from exploiting the region format. This has kept Apple from changing the region format for 15 years.
map2
that takes a function
f and a pair of lists and applies the function f to
corresponding elements of the list, generating a list of the results.
That is, map2 f
([x1,...,xn],
[y1,...,yn])
should
evaluate to
[f(x1,y1),...,
f(xn, yn)]
. If
the lists are of different length, raise the exception
WrongLength
.
exception WrongLength val map2 : (('a * 'b) -> 'c ) -> ('a list * 'b list) -> 'c list
Suppose you have a sequence of data from a thermometer that is sampled every minute. You can create a naïve encoding of the temperature data by creating a list of time/temperature pairs. Here is a sample of a sequence of winter temperatures starting at 12:00 noon with time encoded as minutes from midnight.
Temp = [(720, 32.0), (721, 32.0), (722, 32.0), (723, 32.0), (724, 32.0), (725, 32.1), (726, 32.1), (727, 32.2), (728, 32.2), ..., (780, 33.0)]It would be easy to use
map
to convert this list from
Fahrenheit to Celsius. It is also easy to map a function over two lists
of temperatures producing a third list. For example, you could make a
list of minimum temperatures for each minute of two days by using
Real.min
with the map2
function. To get the
time values to work out, the two temperature lists must start at the
same time and end at the same time.
Notice many of the temperature readings are the same. The above encoding
wastes a lot of space duplicating the same readings for several
minutes. We can save space by removing the redundant data from the list
and only keeping the transitions to new temperatures and the time of the
temperature change. Here is an example of the Temp
list
with the redundant entries removed.
CompactTemp = [(720, 32.0), (725, 32.1), (727, 32.2), ...]A
map2
function is more complicated than with the naive
encoding. Each time/value pair represents the value of the reading until
the transition point. For this reason, the result of a function applied
to both signals will only change values if there is a transition in one
of the two lists. Ensuring the output list doesn't have any redundant
entries makes the problem even more difficult. Here is an example of
taking the minimum temperature per minute of two days. Assume two sets
of data.
Day1 = [(720, 32.0), (725, 32.1), (727, 32.2), ...]) Day2 = [(720, 32.1), (723, 32.0), (726, 31.9), (727, 31.8), (729, 31.7), ...]Here is the result of taking the minimum temperatures of
Day1
and Day2
on a per minute basis.
Result = [(720, 32.0), (726, 31.9), (727, 31.8), (729, 31.7), ...]A signal generalizes the above ideas. The discussion above used finite lists as to record the temperature data. In general, signals should be infinite in both directions with a finite number of transitions. By pairing transition lists with an initial value, you can represent an infinite series of values. The initial value represents the value of the signal before the first transition. In addition, assume that the signal has the value of the last transition forever after the last transition. The type for a signal is now a pair consisting of an initial value and a list of time/value pairs.
type signal = value * (time * value) list;A signal must satisfy the following signal invariants:
An implementation of signals should satisfy the following signature.
signature SIGNAL = sig type time (* Parameter, must be ordered *) type value (* Parameter, must admit equality *) type signal (* Abstract *) (* You can assume the transition list satisfies the signal invariants. *) (* Takes a list and converts it to a signal. *) val fromList : value * (time * value) list -> signal (* Returns true if the transition list obeys invariants, false otherwise. *) val checkList : value * (time * value) list -> bool (* Takes a signal and converts it to a list. *) val toList : signal -> value * (time * value) list (* Maps a function over the values in a signal. *) val map : (value -> value) -> signal -> signal (* Maps a function over a pair of signals. *) val map2 : (value * value -> value) -> signal * signal -> signal (* Convert a signal to a function from time to values *) val eval : signal -> (time -> value) (* Compares two signals. *) val equal : (signal * signal) -> bool end (* signature SIGNAL *)In the
SIGNAL
signature, the types time
and
value
are to be thought of as parameters. We use
where type
to create instances of SIGNAL
with
concrete types for time
and time
. For
example, a temperature signal has the following declaration.
signature TEMP_SIGNAL = SIGNAL where type time = int where type value = real;In order to maintain the signal invariants, the type
time
must be ordered and type value
must admit equality. For this,
we use two signatures.
signature ORDER = sig type t (* parameter *) val compare : t * t -> order end; signature EQUAL = sig type t (* parameter *) val equal : t * t -> bool end;
SIGNAL
given structures for transitions and values.
functor Signal (structure Time : ORDER structure Value : EQUAL) :> SIGNAL where type time = Time.t where type value = Value.tThe type
signal
should be implemented as
type signal = value * (time * value) listas explained above.
Make sure your functions take advantage of and preserve the signal invariants!
The graph of a any function f is the set {(x, f(x)) | x in D} where D is the domain of f. If we take the graph of our example characteristic function FS we get the set:
{..., (9, false), (10, true), (11, true), (12, true), (13, true), (14, false), ..., (22, false), (23, true), (24, true), (25, true), (26, false), ...}.We know that the values before 10 are false, and the values after 26 are false, so we can turn this directly into the following boolean signal:
(false, [(10,true), (14, false), (23, true), (26, false)])
signature BOOL_SIGNAL = SIGNAL where type time = int where type value = bool; structure BoolSignal :> BOOL_SIGNALis provided in the file ass4.sml. The interface for integer sets is given by the following signature.
signature INTSET = sig type intset val int : intset (* set of all integers *) val nat : intset (* set of integers >= 0 *) val empty : intset (* empty set of integers *) val union : intset * intset -> intset val intersect : intset * intset -> intset val difference : intset * intset -> intset val member : int * intset -> bool end;Write a functor
functor IntSet (structure BS : BOOL_SIGNAL) :> INTSET where type intset = BS.signal = struct type intset = BS.signal ... end;Hint: You should find the function
map2
most useful.
A = {(2,1), (5,1), (2,2), (5,2), (2,3), (3,3), (3,4), (3,5), (2,4), (5,4), (2,5), (5,5) }Region operations can be thought of as set operations over sets of pixel locations. For example, if you have the region B = {(2,2),(3,2),(4,2),(5,2)} then the intersection of region A and region B is the set of pixel locations {(2,2),(5,2)}. Representing regions as sets of pixels has a lot of redundant data. To shrink the representation, gather all the pixels for the same row, and removing the y-components from the location pairs and store a single y value with a set of x values. This representation of regions has the form {(Y position, {X set}), ...}. Then take that set and sort it into a list by Y position. The region A in the new format looks like this:
A = [(1, {2,5}), (2, {2,5}), (3, {2,3,4,5}), (4, {2,5}), (5, {2,5})]Operations for this format are similar to operations in the naive set format. To calculate the intersection of regions A and B, calculate the intersection of the X sets for each of the row pairs in A and B. If the row does not exist, assume its X set is empty. In this format notice that some of the rows have the same X set. We can now convert a region into a signal where Y is the transition time, and the X set is the value. The above region A becomes the signal:
SignalA = ({},[(1,{2,5}),(3,{2,3,4,5}),(4,{2,5}),(6,{})])Here the empty set at row 6 means that there are no pixels in the set that have a y component greater that 5. The initial set being empty means that there are no values before the first transition at row 1.
Region
using the structure
BoolSignal
and the functor Signal
satisfying
the signature REGION
.
signature REGION = SIGNAL where type time = int where type value = BoolSignal.signal;
empty
, union
, intersect
and
difference
for regions.
val empty : Region.signal val union : (Region.signal * Region.signal) -> Region.signal val intersect : (Region.signal * Region.signal) -> Region.signal val difference : (Region.signal * Region.signal) -> Region.signalFor this, the operations in structure
IntSet
and
Region.map2
should be very helpful.
fromRectangle
which takes two
opposite corners of a rectangle, and produces a region. You may not
assume the corners are in the proper order.
type rectangle = ((int * int) * (int * int)) val fromRectangle : rectangle -> Region.signal
calcVisRgn
is the core of the Macintosh
windowing system. It takes a list of rectangles, and returns a list of
the visible portions of the windows. Rectangles that come first in the
list obscure rectangles later in the list.
val calcVisRgn = rectangle list -> Region.signal list;
/afs/andrew/scs/cs/15-212-X/studentdir/<your andrew id>/ass4/