8. Courses
A course sheet is a layout with the course drawn on the map in purple. The project file links to the file from your course-setting program, and check and export do the rest: one sheet per course, with control numbers and, for Purple Pen files, the control descriptions. The example examples/rummelsburg-competition-courses goes through all of it with every command and its output. examples/gleisdreieck-training is a smaller one, a club training with two courses.
Where courses come from
The program reads two kinds of files:
- A Purple Pen project,
.ppen. It holds the controls, the courses, bends and gaps of the legs, circle gaps per scale, the control descriptions, special objects such as out-of-bounds areas, forbidden routes and marked routes, the position of each control number, and the print scale of each course. - IOF XML 3.0 with
CourseData, from Purple Pen, Condes, the OCAD course setting or any other program that writes it. It holds controls, courses, the positions of the controls and, if the writer sets them, the position of each number, the course length and the climb.
IOF XML has no control descriptions, special objects, bends or print scales. A sheet from an IOF XML file shows the controls, the legs and the numbers, nothing else. If you want the description box or an out-of-bounds area on the sheet, export the project from Purple Pen and use the .ppen. The program picks the reader from the file extension, .ppen or .xml; any other extension is an error. If your program offers both, the .ppen gives the fuller sheet.
Not everything in a file is printed yet:
- A course with variations (forks and loops in Purple Pen) is left out, and so is a course with alternative controls at one stop.
checksays which. Purple Pen's IOF XML export writes each variation as a course of its own, which prints as a sheet of its own. - An element the program does not know is skipped, with a note.
White-out areas print as masks of the courses they belong to, each on its own, also where they overlap (ADR 0045). The map is missing there; the course overprint, the description box and the footer stay. When a course's assignment names a mask of its own, only that one applies. check lists the white-out areas for information. Chapter 11 describes masks for window and corridor orienteering.
Linking the file: the live link
In the project file, [sources] gets the key courses:
[sources]
map = "rummelsburg.omap"
courses = "courses.ppen"
The path is relative to the project file. The program reads the course file again on every check and every export. If you move a control in Purple Pen, add a course or change a description, save the file and run export. Nothing needs refreshing in the project.
The project file keeps only what the course file does not know. Two kinds of entries refer to it:
[[assignments]]says which layout a course prints on. It names the course as the course file does:[[assignments]] course = "A" layout = "a4-4000"A course without an assignment takes the first layout, and
checksays so as Info.[[numbers]]corrects the position of a control number, either for a layout or for one course (see "Number positions" below). It names the control by its code.
When the course file changes, these entries can name something that no longer exists, for example a renamed course or a deleted control. The program does not drop them. It reports each one with a note and keeps it in the project file, so a renamed course does not cost you its settings. The other courses load as usual. Fix the name in the project file, or delete the entry. If a position in the course file has moved since you made a correction, check tells you that too. The correction still applies, and you decide whether it still fits.
Coordinates and the fit
Before it prints, the program fits the course to the map, and it stops when the fit looks wrong. A course on the wrong map or upside down would send runners to the wrong place. These checks run:
The y axis. Purple Pen, Condes and OCAD write the y axis upwards, OOM counts downwards. Purple Pen always writes it upwards, so the program reads a
.ppenthat way. If the course does not fit the map like that, the fit stops; it never prints a Purple Pen course mirrored. An IOF XML file does not say which axis it uses. The program tries both readings and takes the one that puts every control on the map. With latitude and longitude in the file and a georeferenced map, the coordinates decide. If both readings fit and nothing decides, the program takes "up" and warns, because a course near the middle of the map fits both ways. Look at a sheet, then confirm the axis in the project file:[sources] course_y_axis = "up" # or "down"An IOF XML file that was written downwards and fits both ways prints upside down until you set it. That is the one case where the program asks you to look. A Purple Pen project needs no
course_y_axis. If you set"down"for a.ppenanyway, the program follows it and warns, because the course then prints mirrored.The map scale. If the course file names a map scale, it must equal the scale of the map. If it differs, the fit stops with "the course file is drawn for 1:…, the map is 1:…". Export the courses from the course-setting program for the map's scale. The program never rescales positions. A file without a map scale gets an Info line. A map that states no scale of its own stops the fit with "the course file is drawn for 1:…, and the map states no scale"; set the scale in OOM.
The georeference. If the map is georeferenced in UTM and a control has both a map position and latitude and longitude, the program compares them. A control more than 20 m from where its latitude and longitude put it stops the fit with "control … lies … m from where its lat/lng put it". Latitude and longitude from a GNSS reading in the field are good to 5 or 10 m. A wrong map, zone or datum shifts every control by hundreds of metres. What to do depends on the cause:
- The map's georeferencing is wrong or belongs to another map. Fix it in OOM.
- The course file belongs to another map. Take the right file.
- Only the latitude and longitude are wrong or unwanted. Set
course_georef_check = falseunder[sources]. The map positions then place the course alone, andchecktells you with an Info line that it ignored the coordinates.
Other coordinate systems than UTM are not checked yet;
checksays so as Info.Outside the map. If no reading of the y axis puts all controls on the map, the fit stops with "only … of … controls lie on the map; the course file belongs to another map, uses another origin or another y axis". Check that you linked the right course file and the right map. If you set
course_y_axis, check that it is right. Controls that sit up to 10 mm outside the drawn map still count, so a start or finish at the edge of a cut-out works.Latitude and longitude only. A control with latitude and longitude but no map position is placed through the map's georeferencing. If the map cannot do that, or you switched the check off, the fit stops with "controls … have only lat/lng and cannot be placed", followed by the reason.
When a check fails, check and export print the error with the file and stop for the whole course file, so a bad file never produces a sheet. The exit code is 1.
Course sheets and the overprint

The sheet of course "A" (2.4 km, eight controls) at 1:4000 with overprint, numbers, title block and description box. The course was made with freemapper-courseplan 0.14.1. Map: Remo Madella, MOO Challenge Berlin 2025.
With a course file, export writes one PDF per course instead of one per layout (see "Export" below). A course sheet is the layout the course is assigned to, with these changes:
- The map frame centres on the course, unless the layout sets
map_frame.centre_map_mm. The program centres on the controls and bends and widens the frame for a special object only if the object still fits. Course sheets are usually cut from a larger map, so setmap_frame.crop = true, asinit --coursesdoes. A map larger than the frame is expected on a course sheet and is only an Info line. A control that lies outside the frame or across its edge is a note. - The program draws the course over the map in purple (35/85/0/0 CMYK in the rule profile) and with the symbol sizes that the rule profile sets for the map kind, from ISSprOM 2019-2 and ISOM 2017-2. These are the sizes at the base scale (1:4000 sprint, 1:15000 forest). On the sheet they take the size the standard sets for the print scale, the course factor: 133 % at 1:3000 on a sprint map, 150 % at 1:10000 on a forest map. That does not depend on the scale the map file is drawn at. A forest map drawn at 1:10000 gets 7.5 mm circles at 1:10000 and 5.0 mm circles at 1:15000. The summary line names the symbol factor, which is for the map's own symbols.
- The purple has two layers, as the IOF's "Printing and Colour Definitions" (Rev. 4) require. The lower purple holds the start, the controls, the legs, the finish, forbidden lines and crossing points. It lies below black, brown and blue 100 %, so the map's black detail stays readable over the circle. The upper purple holds the control numbers, map exchanges, out-of-bounds areas, forbidden routes, marked routes and texts. It lies over the whole map.
- Both layers are painted in
Multiplyblend mode. The map shows through the purple everywhere, and black under the purple stays black. An office printer has no spot colour and no overprint flag, so the PDF has to show the result itself. - The text elements know four more placeholders:
{course}(the course name),{length_km}(the length in kilometres, without unit),{climb_m}(the climb in metres, without unit) and{controls}(the number of controls). A placeholder stays empty, with an Info line, if the file has no value for the course or if the sheet has no course. For example"{event} · Course {course}"names the course in the title block.init --courseswrites{event} · {course}. - If the course's assignment names a mask, or the course has white-out areas from Purple Pen, the map is missing in the masked area. The overprint and the sheet elements stay (chapter 11).
The program also checks that no sheet element hides part of the course. If a title block, logo, text, scale check bar, footer line or the description box covers a control, the start or the finish, the check warns and names the element and the controls. Move the element with its anchor.
The control description box
A layout element of kind control_descriptions shows the control descriptions of the course as the IOF defines them:
[[layouts.elements]]
id = "descriptions"
kind = "control_descriptions"
cell_mm = 6.0 # optional, 4 to 10, default from the rule profile
anchor = { to = "map_frame", corner = "top_right", offset_mm = [2.0, 2.0] }
The box is laid out the way Purple Pen lays it out. It shows the event title, the course with its length and climb, the start, one row per control with number, code and the symbols for the columns C to H, the rows for marked routes and map exchanges with their length, and the finish. The secondary title of a course from Purple Pen gets its own row, and a map flip the row 15.6. A score course lists its controls by code, the way Purple Pen does.

The description box of course "A" with title, length (2.4 km), start, eight controls and finish. Map: Remo Madella, MOO Challenge Berlin 2025.
The box has a white background and is drawn in Arimo. A cell is 6 mm by default. cell_mm sets it from 4 to 10 mm. The box keeps its size at every print scale, as an element on paper should. If a 1:3000 sheet needs more room for the map, make the cells smaller.
The box does not appear if it would reach past the page edge, because a cut-off description could hide a control. check warns about that, and about a course where no control has descriptions. A layout without the element simply has no box, and check says so as Info. The same holds for IOF XML. A symbol that the course file's description standard does not know leaves its cell empty, and check names it. The description standards 2004, 2018 and 2024 are read.
Not done yet:
- A map issue point is read as a start. Its own row, with the length of the marked route to the start, is missing.
- A leg that is only partly flagged is drawn and described as unflagged.
- A long course is not split into several columns.
- The box has no column for text next to the symbols, and no key for custom symbols.
Number positions
Where a control number sits matters on a crowded sprint map, and several people may want to decide. The program looks at four levels, and the more specific one wins:
- Automatic placement. It applies only to a numbered control for which the course file gives no position.
- The course file. Purple Pen stores the position per course, relative to the control. For a
.ppen, the program converts the offset at the course's print scale and with Purple Pen's setting for scaling course objects ("Scale sizes of items"). For IOF XML, it takesMapTextPosition. - A correction for the layout: a
[[numbers]]table withlayout. - A correction for the sheet of one course: a
[[numbers]]table withcourse.
A correction names the control, a direction in degrees (0 is east, counter-clockwise) and optionally a gap:
[[numbers]]
control = "49"
course = "A"
direction = 161.0
gap_mm = 0.5
The gap is the shortest distance in mm at the base scale between the outer edge of the circle line and the number's box. It grows with the course factor like the circle, so a number keeps its look at every print scale. A gap_mm lies above 0 and at most 10 mm. Without it, the correction keeps the gap of the level below. numbers accept also writes a [numbers.base_seen] table with the position the file had when you made the correction.
The rule profile sets the range in which a gap should lie. In iof-print it is 0.5 to 5 mm. A number outside the range prints where it is, and check warns. A profile rule with only a minimum makes automatic placement keep to that gap, as freemapper-courseplan does; check then accepts any larger gap up to 10 mm.
When the program places a number itself, it follows four rules. The user rules of orienteering come first:
- The number covers no point symbol and no line symbol of the map. This rule is hard.
- The number goes behind the control, in line with the leg that comes in. For the first control that leg comes from the start.
- If the box behind the control would come within half a line width of the outgoing leg, the number goes on the outer bisector, between the two legs on their outer side.
- If the preferred place is blocked, the free place that deviates least from it wins.

Controls 4 to 6 of course "A" with circle, leg and number. Map: Remo Madella, MOO Challenge Berlin 2025.
The program never moves a number on its own. It prints where the level puts it, and check scores every number against the overprint, the sheet elements, the edge of the map frame and the map's own symbols. If a number lies on one of them, the note names what it covers (map symbols by their code) and proposes a direction and a gap. Where no place is free, the proposal is the least covered one.
The search has a limit per sheet: 2 million candidate positions, and 200 million for all sheets of one course file, shared equally. Real courses stay far below it. A sheet that reaches it says so in a note; the numbers the search did not get to stay where their level puts them, and their conflicts come without a proposal.
To take a proposal:
freemapper-print numbers accept citysprint --course A --control 49 --sheet
--sheet makes the correction for this course's sheet (level 4). --layout-wide makes it for every course that prints on the course's layout (level 3). Exactly one of the two is needed. Without --direction and --gap, the command takes the proposal from check. With both, it writes your values. If check has no proposal because the number has no conflict, the command stops and asks for the two values.
The proposal was found on the sheet of the course you name. With --layout-wide and no values of your own, the command draws every other course on that layout that prints the number with the correction added, and writes it only when none of them gets a conflict there. Otherwise it names those courses and stops with exit code 2. Use --sheet for the one course, or give --direction and --gap for all of them.
numbers accept appends a [[numbers]] table to the end of the project file as text. Everything before it stays as it is, comments included. The new lines end like the file's (CRLF or LF), a project file that is a link is replaced by a regular file (the link's target keeps its content), and the file keeps its permissions. It then loads the file again, and if that fails, it restores the old content. It refuses a second correction for the same control and scope. To change one, edit the file. Afterwards run check to see the result.
Export
freemapper-print export citysprint
freemapper-print export citysprint --course A --course B
freemapper-print export citysprint --single-file
freemapper-print export citysprint --layout a4-4000
With a course file, export writes one PDF per course into the folder out, named after the course: A.pdf. Characters that a file name cannot hold become -. Names that differ only in case get " (2)".
--course <NAME>exports only the named course. It can be repeated. A name the file does not have is an error that lists the courses it does have, with exit code 2.--single-filewrites all courses into one PDF, one page per course, each on the paper of its layout. The file is named after the event title with " - all courses" ("- alle Bahnen" on German sheets). It cannot be combined with--layout.--layout <ID>exports a layout as before, without any course, the map alone with title and scale check bar. With a course file it cannot be combined with--courseor--single-file.--out-dirworks as before.
A project without a course file exports exactly as in chapter 5.
Starting from a course file: init --courses
freemapper-print init citysprint --map rummelsburg.omap --title "Stadtsprint Rummelsburg" --courses courses.ppen
init reads and fits the course file first. A file that does not fit the map stops init before it writes anything. For a .ppen it then creates one layout per print scale in the file, an assignment for every course, a description box in the top-right corner of the map frame, and map_frame.crop = true. A course without a print scale takes the map scale. For IOF XML it creates one layout at the map scale and no description box, because the format has none. Each layout id names its print scale, such as a4-4000. In both cases the title block starts with {event} · {course}, so that each sheet names its course. init says where each value came from and names the course file as the project file does, relative to the project folder.
For an IOF XML file, init also copies the event date and the organiser, if the file names them, into [event] as date and organiser, so that {date} and {organiser} are filled. A .ppen file has neither. Only init copies them; later changes in the course file do not reach the project.
Treat the file as a start. init picks paper and passe-partout for the layout as a sheet of its own; whether each course fits the frame shows in check. Change paper and passe-partout to your liking, then run check. For an IOF XML file the first check may report the assumed y axis; confirm it as described above.
Checking courses
check goes through every course. For each it prints one line with the layout, the paper, the print scale, the map scale and the factors, followed by the notes. Then it checks each layout as a sheet without course, as export --layout writes it. Notes about the course file itself, from reading and fitting it, come once before the first course. The last line reads "Checked 3 courses and 2 layout sheets, no notes." or "With notes: 1 of 3 courses, 0 of 2 layout sheets." The exit code is 0 when nothing needs your attention (Info lines do not count) and 1 otherwise. A script can run check before export.
The notes and what to do are in chapter 10.