Skip to content

Instantly share code, notes, and snippets.

@ajesler
Created July 15, 2025 10:35
Show Gist options
  • Select an option

  • Save ajesler/777becf1fd7cd3c142d47445082fbfdb to your computer and use it in GitHub Desktop.

Select an option

Save ajesler/777becf1fd7cd3c142d47445082fbfdb to your computer and use it in GitHub Desktop.
// References
// https://developers.google.com/apps-script/reference/spreadsheet/
// https://developers.google.com/apps-script/reference/xml-service
// https://github.com/international-orienteering-federation/datastandard-v3/blob/master/IOF.xsd
// https://github.com/international-orienteering-federation/datastandard-v3/blob/master/examples/CourseData_Individual_Step2.xml
// https://condes.net/onlinehelp/?HIDD_COPYOBJECTS.html
// See https://docs.google.com/document/d/1qSCO3uMm6tzxSS41zrhS6jWHufgTpipzf4SsAsMLLXQ for instructions on how to use this
// Convert a spreadsheet of control information into a IOF formatted XML file of controls that can be imported into Condes orienteering software.
function exportAsIOF() {
// Update the values in this section to reflect your setup
// -- BEGIN CHANGE THESE VALUES --
const creator = "Your Name";
const eventName = "My Rogaine";
// This should be the full range of cells your sheet uses
const sheetCellRange = 'A1:M53';
// Get this from the last part of the Google Drive folder URL
// eg https://drive.google.com/drive/u/0/folders/1FeABCaKlhXZYZuoX3i7WIROxEc8588pC
const folderId = "1FeABCaKlhXZYZuoX3i7WIROxEc8588pC";
const fileName = "controls.xml";
// Set these to be the column number in the spreadsheet you want to pull the id, lat, and lng from.
const controlIdColNumber = 3;
const latColNumber = 6;
const lngColNumber = 7;
// -- END CHANGE THESE VALUES --
var sheet = SpreadsheetApp.getActiveSheet();
var rows = sheet.getRange(sheetCellRange).getValues();
rows.shift(); // Get rid of the header rows, we don't need them for this.
const createdAtTimestamp = Utilities.formatDate(
new Date(),
Session.getScriptTimeZone(),
"yyyy-MM-dd HH:mm:ss"
);
const ns = XmlService.getNamespace("http://www.orienteering.org/datastandard/3.0");
let root = XmlService.createElement('CourseData', ns)
.setAttribute("iofVersion", "3.0")
.setAttribute("Creator", creator)
.setAttribute("createTime", createdAtTimestamp);
let event = XmlService.createElement("Event", ns);
event.addContent(XmlService.createElement("Name", ns).setText(eventName))
root.addContent(event);
let raceCourseData = XmlService.createElement("RaceCourseData", ns);
root.addContent(raceCourseData);
rows.forEach((c) => {
// If we are missing any info for the control, skip it.
if(c[controlIdColNumber] == "" || c[latColNumber] == "" || c[lngColNumber] == "") {
return;
}
let control = XmlService.createElement("Control", ns);
control.addContent(XmlService.createElement("Id", ns).setText(c[2]));
let position = XmlService.createElement("Position", ns)
.setAttribute("lat", c[latColNumber])
.setAttribute("lng", c[lngColNumber]);
control.addContent(position);
// This isn't used, but if this element isn't here, condes does not import the control.
let mapPosition = XmlService.createElement("MapPosition", ns)
.setAttribute("unit", "mm")
.setAttribute("x", "1")
.setAttribute("y", "1");
control.addContent(mapPosition);
raceCourseData.addContent(control);
})
let document = XmlService.createDocument(root);
let xmlContent = XmlService.getPrettyFormat().format(document);
// console.log(xml); // Uncomment if you want to see the generated XML
let file = getFileWithName(folderId, fileName);
if(file){
// File exists, so replace the contents
file.setContent(xmlContent);
} else {
// File doesn't exist, so create it
DriveApp.getFolderById(folderId).createFile(fileName, xmlContent);
}
}
function getFileWithName(folderId, fileName) {
var folder = DriveApp.getFolderById(folderId);
var files = folder.getFilesByName(fileName);
while (files.hasNext()) {
var file = files.next();
return file;
}
return null;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment