<!--{{{-->
<link rel='alternate' type='application/rss+xml' title='RSS' href='index.xml' />
<!--}}}-->
Background: #fff
Foreground: #000
PrimaryPale: #8cf
PrimaryLight: #18f
PrimaryMid: #04b
PrimaryDark: #014
SecondaryPale: #ffc
SecondaryLight: #fe8
SecondaryMid: #db4
SecondaryDark: #841
TertiaryPale: #eee
TertiaryLight: #ccc
TertiaryMid: #999
TertiaryDark: #666
Error: #f88
/*{{{*/
body {background:[[ColorPalette::Background]]; color:[[ColorPalette::Foreground]];}

a {color:[[ColorPalette::PrimaryMid]];}
a:hover {background-color:[[ColorPalette::PrimaryMid]]; color:[[ColorPalette::Background]];}
a img {border:0;}

h1,h2,h3,h4,h5,h6 {color:[[ColorPalette::SecondaryDark]]; background:transparent;}
h1 {border-bottom:2px solid [[ColorPalette::TertiaryLight]];}
h2,h3 {border-bottom:1px solid [[ColorPalette::TertiaryLight]];}

.button {color:[[ColorPalette::PrimaryDark]]; border:1px solid [[ColorPalette::Background]];}
.button:hover {color:[[ColorPalette::PrimaryDark]]; background:[[ColorPalette::SecondaryLight]]; border-color:[[ColorPalette::SecondaryMid]];}
.button:active {color:[[ColorPalette::Background]]; background:[[ColorPalette::SecondaryMid]]; border:1px solid [[ColorPalette::SecondaryDark]];}

.header {background:[[ColorPalette::PrimaryMid]];}
.headerShadow {color:[[ColorPalette::Foreground]];}
.headerShadow a {font-weight:normal; color:[[ColorPalette::Foreground]];}
.headerForeground {color:[[ColorPalette::Background]];}
.headerForeground a {font-weight:normal; color:[[ColorPalette::PrimaryPale]];}

.tabSelected {color:[[ColorPalette::PrimaryDark]];
	background:[[ColorPalette::TertiaryPale]];
	border-left:1px solid [[ColorPalette::TertiaryLight]];
	border-top:1px solid [[ColorPalette::TertiaryLight]];
	border-right:1px solid [[ColorPalette::TertiaryLight]];
}
.tabUnselected {color:[[ColorPalette::Background]]; background:[[ColorPalette::TertiaryMid]];}
.tabContents {color:[[ColorPalette::PrimaryDark]]; background:[[ColorPalette::TertiaryPale]]; border:1px solid [[ColorPalette::TertiaryLight]];}
.tabContents .button {border:0;}

#sidebar {}
#sidebarOptions input {border:1px solid [[ColorPalette::PrimaryMid]];}
#sidebarOptions .sliderPanel {background:[[ColorPalette::PrimaryPale]];}
#sidebarOptions .sliderPanel a {border:none;color:[[ColorPalette::PrimaryMid]];}
#sidebarOptions .sliderPanel a:hover {color:[[ColorPalette::Background]]; background:[[ColorPalette::PrimaryMid]];}
#sidebarOptions .sliderPanel a:active {color:[[ColorPalette::PrimaryMid]]; background:[[ColorPalette::Background]];}

.wizard {background:[[ColorPalette::PrimaryPale]]; border:1px solid [[ColorPalette::PrimaryMid]];}
.wizard h1 {color:[[ColorPalette::PrimaryDark]]; border:none;}
.wizard h2 {color:[[ColorPalette::Foreground]]; border:none;}
.wizardStep {background:[[ColorPalette::Background]]; color:[[ColorPalette::Foreground]];
	border:1px solid [[ColorPalette::PrimaryMid]];}
.wizardStep.wizardStepDone {background:[[ColorPalette::TertiaryLight]];}
.wizardFooter {background:[[ColorPalette::PrimaryPale]];}
.wizardFooter .status {background:[[ColorPalette::PrimaryDark]]; color:[[ColorPalette::Background]];}
.wizard .button {color:[[ColorPalette::Foreground]]; background:[[ColorPalette::SecondaryLight]]; border: 1px solid;
	border-color:[[ColorPalette::SecondaryPale]] [[ColorPalette::SecondaryDark]] [[ColorPalette::SecondaryDark]] [[ColorPalette::SecondaryPale]];}
.wizard .button:hover {color:[[ColorPalette::Foreground]]; background:[[ColorPalette::Background]];}
.wizard .button:active {color:[[ColorPalette::Background]]; background:[[ColorPalette::Foreground]]; border: 1px solid;
	border-color:[[ColorPalette::PrimaryDark]] [[ColorPalette::PrimaryPale]] [[ColorPalette::PrimaryPale]] [[ColorPalette::PrimaryDark]];}

.wizard .notChanged {background:transparent;}
.wizard .changedLocally {background:#80ff80;}
.wizard .changedServer {background:#8080ff;}
.wizard .changedBoth {background:#ff8080;}
.wizard .notFound {background:#ffff80;}
.wizard .putToServer {background:#ff80ff;}
.wizard .gotFromServer {background:#80ffff;}

#messageArea {border:1px solid [[ColorPalette::SecondaryMid]]; background:[[ColorPalette::SecondaryLight]]; color:[[ColorPalette::Foreground]];}
#messageArea .button {color:[[ColorPalette::PrimaryMid]]; background:[[ColorPalette::SecondaryPale]]; border:none;}

.popupTiddler {background:[[ColorPalette::TertiaryPale]]; border:2px solid [[ColorPalette::TertiaryMid]];}

.popup {background:[[ColorPalette::TertiaryPale]]; color:[[ColorPalette::TertiaryDark]]; border-left:1px solid [[ColorPalette::TertiaryMid]]; border-top:1px solid [[ColorPalette::TertiaryMid]]; border-right:2px solid [[ColorPalette::TertiaryDark]]; border-bottom:2px solid [[ColorPalette::TertiaryDark]];}
.popup hr {color:[[ColorPalette::PrimaryDark]]; background:[[ColorPalette::PrimaryDark]]; border-bottom:1px;}
.popup li.disabled {color:[[ColorPalette::TertiaryMid]];}
.popup li a, .popup li a:visited {color:[[ColorPalette::Foreground]]; border: none;}
.popup li a:hover {background:[[ColorPalette::SecondaryLight]]; color:[[ColorPalette::Foreground]]; border: none;}
.popup li a:active {background:[[ColorPalette::SecondaryPale]]; color:[[ColorPalette::Foreground]]; border: none;}
.popupHighlight {background:[[ColorPalette::Background]]; color:[[ColorPalette::Foreground]];}
.listBreak div {border-bottom:1px solid [[ColorPalette::TertiaryDark]];}

.tiddler .defaultCommand {font-weight:bold;}

.shadow .title {color:[[ColorPalette::TertiaryDark]];}

.title {color:[[ColorPalette::SecondaryDark]];}
.subtitle {color:[[ColorPalette::TertiaryDark]];}

.toolbar {color:[[ColorPalette::PrimaryMid]];}
.toolbar a {color:[[ColorPalette::TertiaryLight]];}
.selected .toolbar a {color:[[ColorPalette::TertiaryMid]];}
.selected .toolbar a:hover {color:[[ColorPalette::Foreground]];}

.tagging, .tagged {border:1px solid [[ColorPalette::TertiaryPale]]; background-color:[[ColorPalette::TertiaryPale]];}
.selected .tagging, .selected .tagged {background-color:[[ColorPalette::TertiaryLight]]; border:1px solid [[ColorPalette::TertiaryMid]];}
.tagging .listTitle, .tagged .listTitle {color:[[ColorPalette::PrimaryDark]];}
.tagging .button, .tagged .button {border:none;}

.footer {color:[[ColorPalette::TertiaryLight]];}
.selected .footer {color:[[ColorPalette::TertiaryMid]];}

.error, .errorButton {color:[[ColorPalette::Foreground]]; background:[[ColorPalette::Error]];}
.warning {color:[[ColorPalette::Foreground]]; background:[[ColorPalette::SecondaryPale]];}
.lowlight {background:[[ColorPalette::TertiaryLight]];}

.zoomer {background:none; color:[[ColorPalette::TertiaryMid]]; border:3px solid [[ColorPalette::TertiaryMid]];}

.imageLink, #displayArea .imageLink {background:transparent;}

.annotation {background:[[ColorPalette::SecondaryLight]]; color:[[ColorPalette::Foreground]]; border:2px solid [[ColorPalette::SecondaryMid]];}

.viewer .listTitle {list-style-type:none; margin-left:-2em;}
.viewer .button {border:1px solid [[ColorPalette::SecondaryMid]];}
.viewer blockquote {border-left:3px solid [[ColorPalette::TertiaryDark]];}

.viewer table, table.twtable {border:2px solid [[ColorPalette::TertiaryDark]];}
.viewer th, .viewer thead td, .twtable th, .twtable thead td {background:[[ColorPalette::SecondaryMid]]; border:1px solid [[ColorPalette::TertiaryDark]]; color:[[ColorPalette::Background]];}
.viewer td, .viewer tr, .twtable td, .twtable tr {border:1px solid [[ColorPalette::TertiaryDark]];}

.viewer pre {border:1px solid [[ColorPalette::SecondaryLight]]; background:[[ColorPalette::SecondaryPale]];}
.viewer code {color:[[ColorPalette::SecondaryDark]];}
.viewer hr {border:0; border-top:dashed 1px [[ColorPalette::TertiaryDark]]; color:[[ColorPalette::TertiaryDark]];}

.highlight, .marked {background:[[ColorPalette::SecondaryLight]];}

.editor input {border:1px solid [[ColorPalette::PrimaryMid]];}
.editor textarea {border:1px solid [[ColorPalette::PrimaryMid]]; width:100%;}
.editorFooter {color:[[ColorPalette::TertiaryMid]];}
.readOnly {background:[[ColorPalette::TertiaryPale]];}

#backstageArea {background:[[ColorPalette::Foreground]]; color:[[ColorPalette::TertiaryMid]];}
#backstageArea a {background:[[ColorPalette::Foreground]]; color:[[ColorPalette::Background]]; border:none;}
#backstageArea a:hover {background:[[ColorPalette::SecondaryLight]]; color:[[ColorPalette::Foreground]]; }
#backstageArea a.backstageSelTab {background:[[ColorPalette::Background]]; color:[[ColorPalette::Foreground]];}
#backstageButton a {background:none; color:[[ColorPalette::Background]]; border:none;}
#backstageButton a:hover {background:[[ColorPalette::Foreground]]; color:[[ColorPalette::Background]]; border:none;}
#backstagePanel {background:[[ColorPalette::Background]]; border-color: [[ColorPalette::Background]] [[ColorPalette::TertiaryDark]] [[ColorPalette::TertiaryDark]] [[ColorPalette::TertiaryDark]];}
.backstagePanelFooter .button {border:none; color:[[ColorPalette::Background]];}
.backstagePanelFooter .button:hover {color:[[ColorPalette::Foreground]];}
#backstageCloak {background:[[ColorPalette::Foreground]]; opacity:0.6; filter:alpha(opacity=60);}
/*}}}*/
/*{{{*/
* html .tiddler {height:1%;}

body {font-size:.75em; font-family:arial,helvetica; margin:0; padding:0;}

h1,h2,h3,h4,h5,h6 {font-weight:bold; text-decoration:none;}
h1,h2,h3 {padding-bottom:1px; margin-top:1.2em;margin-bottom:0.3em;}
h4,h5,h6 {margin-top:1em;}
h1 {font-size:1.35em;}
h2 {font-size:1.25em;}
h3 {font-size:1.1em;}
h4 {font-size:1em;}
h5 {font-size:.9em;}

hr {height:1px;}

a {text-decoration:none;}

dt {font-weight:bold;}

ol {list-style-type:decimal;}
ol ol {list-style-type:lower-alpha;}
ol ol ol {list-style-type:lower-roman;}
ol ol ol ol {list-style-type:decimal;}
ol ol ol ol ol {list-style-type:lower-alpha;}
ol ol ol ol ol ol {list-style-type:lower-roman;}
ol ol ol ol ol ol ol {list-style-type:decimal;}

.txtOptionInput {width:11em;}

#contentWrapper .chkOptionInput {border:0;}

.externalLink {text-decoration:underline;}

.indent {margin-left:3em;}
.outdent {margin-left:3em; text-indent:-3em;}
code.escaped {white-space:nowrap;}

.tiddlyLinkExisting {font-weight:bold;}
.tiddlyLinkNonExisting {font-style:italic;}

/* the 'a' is required for IE, otherwise it renders the whole tiddler in bold */
a.tiddlyLinkNonExisting.shadow {font-weight:bold;}

#mainMenu .tiddlyLinkExisting,
	#mainMenu .tiddlyLinkNonExisting,
	#sidebarTabs .tiddlyLinkNonExisting {font-weight:normal; font-style:normal;}
#sidebarTabs .tiddlyLinkExisting {font-weight:bold; font-style:normal;}

.header {position:relative;}
.header a:hover {background:transparent;}
.headerShadow {position:relative; padding:4.5em 0 1em 1em; left:-1px; top:-1px;}
.headerForeground {position:absolute; padding:4.5em 0 1em 1em; left:0; top:0;}

.siteTitle {font-size:3em;}
.siteSubtitle {font-size:1.2em;}

#mainMenu {position:absolute; left:0; width:10em; text-align:right; line-height:1.6em; padding:1.5em 0.5em 0.5em 0.5em; font-size:1.1em;}

#sidebar {position:absolute; right:3px; width:16em; font-size:.9em;}
#sidebarOptions {padding-top:0.3em;}
#sidebarOptions a {margin:0 0.2em; padding:0.2em 0.3em; display:block;}
#sidebarOptions input {margin:0.4em 0.5em;}
#sidebarOptions .sliderPanel {margin-left:1em; padding:0.5em; font-size:.85em;}
#sidebarOptions .sliderPanel a {font-weight:bold; display:inline; padding:0;}
#sidebarOptions .sliderPanel input {margin:0 0 0.3em 0;}
#sidebarTabs .tabContents {width:15em; overflow:hidden;}

.wizard {padding:0.1em 1em 0 2em;}
.wizard h1 {font-size:2em; font-weight:bold; background:none; padding:0; margin:0.4em 0 0.2em;}
.wizard h2 {font-size:1.2em; font-weight:bold; background:none; padding:0; margin:0.4em 0 0.2em;}
.wizardStep {padding:1em 1em 1em 1em;}
.wizard .button {margin:0.5em 0 0; font-size:1.2em;}
.wizardFooter {padding:0.8em 0.4em 0.8em 0;}
.wizardFooter .status {padding:0 0.4em; margin-left:1em;}
.wizard .button {padding:0.1em 0.2em;}

#messageArea {position:fixed; top:2em; right:0; margin:0.5em; padding:0.5em; z-index:2000; _position:absolute;}
.messageToolbar {display:block; text-align:right; padding:0.2em;}
#messageArea a {text-decoration:underline;}

.tiddlerPopupButton {padding:0.2em;}
.popupTiddler {position: absolute; z-index:300; padding:1em; margin:0;}

.popup {position:absolute; z-index:300; font-size:.9em; padding:0; list-style:none; margin:0;}
.popup .popupMessage {padding:0.4em;}
.popup hr {display:block; height:1px; width:auto; padding:0; margin:0.2em 0;}
.popup li.disabled {padding:0.4em;}
.popup li a {display:block; padding:0.4em; font-weight:normal; cursor:pointer;}
.listBreak {font-size:1px; line-height:1px;}
.listBreak div {margin:2px 0;}

.tabset {padding:1em 0 0 0.5em;}
.tab {margin:0 0 0 0.25em; padding:2px;}
.tabContents {padding:0.5em;}
.tabContents ul, .tabContents ol {margin:0; padding:0;}
.txtMainTab .tabContents li {list-style:none;}
.tabContents li.listLink { margin-left:.75em;}

#contentWrapper {display:block;}
#splashScreen {display:none;}

#displayArea {margin:1em 17em 0 14em;}

.toolbar {text-align:right; font-size:.9em;}

.tiddler {padding:1em 1em 0;}

.missing .viewer,.missing .title {font-style:italic;}

.title {font-size:1.6em; font-weight:bold;}

.missing .subtitle {display:none;}
.subtitle {font-size:1.1em;}

.tiddler .button {padding:0.2em 0.4em;}

.tagging {margin:0.5em 0.5em 0.5em 0; float:left; display:none;}
.isTag .tagging {display:block;}
.tagged {margin:0.5em; float:right;}
.tagging, .tagged {font-size:0.9em; padding:0.25em;}
.tagging ul, .tagged ul {list-style:none; margin:0.25em; padding:0;}
.tagClear {clear:both;}

.footer {font-size:.9em;}
.footer li {display:inline;}

.annotation {padding:0.5em; margin:0.5em;}

* html .viewer pre {width:99%; padding:0 0 1em 0;}
.viewer {line-height:1.4em; padding-top:0.5em;}
.viewer .button {margin:0 0.25em; padding:0 0.25em;}
.viewer blockquote {line-height:1.5em; padding-left:0.8em;margin-left:2.5em;}
.viewer ul, .viewer ol {margin-left:0.5em; padding-left:1.5em;}

.viewer table, table.twtable {border-collapse:collapse; margin:0.8em 1.0em;}
.viewer th, .viewer td, .viewer tr,.viewer caption,.twtable th, .twtable td, .twtable tr,.twtable caption {padding:3px;}
table.listView {font-size:0.85em; margin:0.8em 1.0em;}
table.listView th, table.listView td, table.listView tr {padding:0 3px 0 3px;}

.viewer pre {padding:0.5em; margin-left:0.5em; font-size:1.2em; line-height:1.4em; overflow:auto;}
.viewer code {font-size:1.2em; line-height:1.4em;}

.editor {font-size:1.1em;}
.editor input, .editor textarea {display:block; width:100%; font:inherit;}
.editorFooter {padding:0.25em 0; font-size:.9em;}
.editorFooter .button {padding-top:0; padding-bottom:0;}

.fieldsetFix {border:0; padding:0; margin:1px 0px;}

.zoomer {font-size:1.1em; position:absolute; overflow:hidden;}
.zoomer div {padding:1em;}

* html #backstage {width:99%;}
* html #backstageArea {width:99%;}
#backstageArea {display:none; position:relative; overflow: hidden; z-index:150; padding:0.3em 0.5em;}
#backstageToolbar {position:relative;}
#backstageArea a {font-weight:bold; margin-left:0.5em; padding:0.3em 0.5em;}
#backstageButton {display:none; position:absolute; z-index:175; top:0; right:0;}
#backstageButton a {padding:0.1em 0.4em; margin:0.1em;}
#backstage {position:relative; width:100%; z-index:50;}
#backstagePanel {display:none; z-index:100; position:absolute; width:90%; margin-left:3em; padding:1em;}
.backstagePanelFooter {padding-top:0.2em; float:right;}
.backstagePanelFooter a {padding:0.2em 0.4em;}
#backstageCloak {display:none; z-index:20; position:absolute; width:100%; height:100px;}

.whenBackstage {display:none;}
.backstageVisible .whenBackstage {display:block;}
/*}}}*/
/***
StyleSheet for use when a translation requires any css style changes.
This StyleSheet can be used directly by languages such as Chinese, Japanese and Korean which need larger font sizes.
***/
/*{{{*/
body {font-size:0.8em;}
#sidebarOptions {font-size:1.05em;}
#sidebarOptions a {font-style:normal;}
#sidebarOptions .sliderPanel {font-size:0.95em;}
.subtitle {font-size:0.8em;}
.viewer table.listView {font-size:0.95em;}
/*}}}*/
/*{{{*/
@media print {
#mainMenu, #sidebar, #messageArea, .toolbar, #backstageButton, #backstageArea {display: none !important;}
#displayArea {margin: 1em 1em 0em;}
noscript {display:none;} /* Fixes a feature in Firefox 1.5.0.2 where print preview displays the noscript content */
}
/*}}}*/
<!--{{{-->
<div class='header' macro='gradient vert [[ColorPalette::PrimaryLight]] [[ColorPalette::PrimaryMid]]'>
<div class='headerShadow'>
<span class='siteTitle' refresh='content' tiddler='SiteTitle'></span>&nbsp;
<span class='siteSubtitle' refresh='content' tiddler='SiteSubtitle'></span>
</div>
<div class='headerForeground'>
<span class='siteTitle' refresh='content' tiddler='SiteTitle'></span>&nbsp;
<span class='siteSubtitle' refresh='content' tiddler='SiteSubtitle'></span>
</div>
</div>
<div id='mainMenu' refresh='content' tiddler='MainMenu'></div>
<div id='sidebar'>
<div id='sidebarOptions' refresh='content' tiddler='SideBarOptions'></div>
<div id='sidebarTabs' refresh='content' force='true' tiddler='SideBarTabs'></div>
</div>
<div id='displayArea'>
<div id='messageArea'></div>
<div id='tiddlerDisplay'></div>
</div>
<!--}}}-->
<!--{{{-->
<div class='toolbar' macro='toolbar [[ToolbarCommands::ViewToolbar]]'></div>
<div class='title' macro='view title'></div>
<div class='subtitle'><span macro='view modifier link'></span>, <span macro='view modified date'></span> (<span macro='message views.wikified.createdPrompt'></span> <span macro='view created date'></span>)</div>
<div class='tagging' macro='tagging'></div>
<div class='tagged' macro='tags'></div>
<div class='viewer' macro='view text wikified'></div>
<div class='tagClear'></div>
<!--}}}-->
<!--{{{-->
<div class='toolbar' macro='toolbar [[ToolbarCommands::EditToolbar]]'></div>
<div class='title' macro='view title'></div>
<div class='editor' macro='edit title'></div>
<div macro='annotations'></div>
<div class='editor' macro='edit text'></div>
<div class='editor' macro='edit tags'></div><div class='editorFooter'><span macro='message views.editor.tagPrompt'></span><span macro='tagChooser excludeLists'></span></div>
<!--}}}-->
To get started with this blank [[TiddlyWiki]], you'll need to modify the following tiddlers:
* [[SiteTitle]] & [[SiteSubtitle]]: The title and subtitle of the site, as shown above (after saving, they will also appear in the browser title bar)
* [[MainMenu]]: The menu (usually on the left)
* [[DefaultTiddlers]]: Contains the names of the tiddlers that you want to appear when the TiddlyWiki is opened
You'll also need to enter your username for signing your edits: <<option txtUserName>>
These [[InterfaceOptions]] for customising [[TiddlyWiki]] are saved in your browser

Your username for signing your edits. Write it as a [[WikiWord]] (eg [[JoeBloggs]])

<<option txtUserName>>
<<option chkSaveBackups>> [[SaveBackups]]
<<option chkAutoSave>> [[AutoSave]]
<<option chkRegExpSearch>> [[RegExpSearch]]
<<option chkCaseSensitiveSearch>> [[CaseSensitiveSearch]]
<<option chkAnimate>> [[EnableAnimations]]

----
Also see [[AdvancedOptions]]
<<importTiddlers>>
div.timeline-frame {
  border: 1px solid #BEBEBE;
  overflow: hidden;
} 
                                             
div.timeline-axis {
  border-color: #BEBEBE;
  border-width: 1px;
  border-top-style: solid;
}  
div.timeline-axis-grid {
  border-left-style: solid;
  border-width: 1px;
}
div.timeline-axis-grid-minor {
  border-color: #e5e5e5;
}  
div.timeline-axis-grid-major {
  border-color: #bfbfbf;
}  
div.timeline-axis-text {
  color: #4D4D4D;
  padding: 3px;
  white-space: nowrap;
}  

div.timeline-axis-text-minor {
}
 
div.timeline-axis-text-major {
}

div.timeline-event {
  color: #1A1A1A;
  border-color: #97B0F8;
  background-color: #D5DDF6;
  
  
  display: inline-block;

}

div.timeline-event-selected {
  border-color: #FFC200;
  background-color: #FFF785;
}


div.timeline-event-box {
  text-align: center;   
  border-style: solid;
  border-width: 0px;  
  border-radius: 0px;  
  -moz-border-radius: 5px; /* For Firefox 3.6 and older */ 
}  

div.timeline-event-dot {
  border-style: solid;
  border-width: 5px;
  border-radius: 5px;
  -moz-border-radius: 5px;  /* For Firefox 3.6 and older */ 
}

div.timeline-event-range {
  border-style: solid;
  border-width: 1px;
  border-radius: 2px;
  -moz-border-radius: 2px;  /* For Firefox 3.6 and older */
}

div.timeline-event-line {
  border-left-width: 1px;
  border-left-style: solid;
} 

div.timeline-event-content {
  margin: 0px;
  white-space: nowrap;
  overflow: hidden;
}

div.timeline-groups-axis {
  border-color: #BEBEBE;
  border-width: 1px;
}
div.timeline-groups-text {
  color: #4D4D4D;
  padding-left: 10px;
  padding-right: 10px;
}

div.timeline-currenttime {
  background-color: #FF7F6E;
  width: 2px;
}

div.timeline-customtime {
  background-color: #6E94FF;
  width: 2px;
  cursor: move;
}

div.timeline-navigation {
  font-family: arial;
  font-size: 20px;
  font-weight: bold;
  color: gray;

  border: 1px solid #BEBEBE;
  background-color: #F5F5F5;
  border-radius: 2px;
  -moz-border-radius: 2px;  /* For Firefox 3.6 and older */
}

div.timeline-navigation-new, div.timeline-navigation-delete, 
    div.timeline-navigation-zoom-in,  div.timeline-navigation-zoom-out, 
    div.timeline-navigation-move-left, div.timeline-navigation-move-right {
  cursor: pointer;
  padding: 10px 10px;
  float: left;
  text-decoration: none;
  border-color: #BEBEBE; /* border is used for the separator between new and navigation buttons */
  
  width: 16px;
  height: 16px;
}

div.timeline-navigation-new {
  background: url('collateral/timeline/img/16/new.png') no-repeat center;
}

div.timeline-navigation-delete {
  padding: 0px;
  padding-left: 5px;
  background: url(''collateral/timeline/img/16/delete.png') no-repeat center;
}

div.timeline-navigation-zoom-in {
  background: url(''collateral/timeline/img/16/zoomin.png') no-repeat center;
}

div.timeline-navigation-zoom-out {
  background: url(''collateral/timeline/img/16/zoomout.png') no-repeat center;
}

div.timeline-navigation-move-left {
  background: url(''collateral/timeline/img/16/moveleft.png') no-repeat center;
}

div.timeline-navigation-move-right {
  background: url(''collateral/timeline/img/16/moveright.png') no-repeat center;
}
/***
|''Name:''|CHAPTimelinePlugin|
|''Version:''|0.5 (2012-06-04)|
|''Author:''|AntonJ, based on code from Almende at http://chap.almende.com/timeline.  All rights are theirs where appropriate.  My bit is licenced under the GPL|
|''Adapted By:''||
|''Type:''|Plugin|
|''Requires:''|Nil, although can be manipulated using ForEachTiddlerPlugin|
!Description
This Plugin implements the CHAP Links Timeline
!Usage
Just install the plugin and tag with systemConfig. Put the following in the tiddler you wish to contain the timline.
{{{
<<drawVisualization DataTag>>
}}}
where "DataTag" is the tag allocated to tiddlers which contain the data for the timline events.

Create tiddlers to contain your events using the tag you selected above (eg DataTag).  Put the following slices in the body of the tiddlers (eg date provided):

{{{
StartDate:2012-04-23
EndDate:2012-12-01
Color:Orchid
Text:My Task
Group:Group 1
}}}
I use ISO ISO date format (YYYY-MM-DD) however other formats may work.
Color is one of the standard HTML colors.
Group may be used to put multiple bars on one line.

!Revision History
*v0.5		- Click on bar now opens tiddler
*v0.4		- Added ability to add single date events.
*v0.3 	- Changed from using DataTiddlerPlugin to using slices.  Improved date handling.
* Just started

!Code
***/

// // Blah
//{{{
/**
 * @file timeline.js
 *
 * @brief
 * The Timeline is an interactive visualization chart to visualize events in
 * time, having a start and end date.
 * You can freely move and zoom in the timeline by dragging
 * and scrolling in the Timeline. Items are optionally dragable. The time
 * scale on the axis is adjusted automatically, and supports scales ranging
 * from milliseconds to years.
 *
 * Timeline is part of the CHAP Links library.
 *
 * Timeline is tested on Firefox 3.6, Safari 5.0, Chrome 6.0, Opera 10.6, and
 * Internet Explorer 6+.
 *
 * @license
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy
 * of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 *
 * Copyright (c) 2011-2012 Almende B.V.
 *
 * @author 	Jos de Jong, <jos@almende.org>
 * @date    2012-03-22
 */


/*
 * TODO
 *
 * Add methods deleteItem, addItem, changeItem to the GWT wrapper
 * Add moving items from one group to another
 * Add options for a minimum and maximum zoom level
 * Add zooming with pinching on Android
 *
 * Bug: neglect items when they have no valid start/end, instead of throwing an error
 * Bug: Pinching on ipad does not work very well, sometimes the page will zoom when pinching vertically
 * Bug: cannot set max width for an item, like div.timeline-event-content {white-space: normal; max-width: 100px;}
 * Bug on IE in Quirks mode. When you have groups, and delete an item, the groups become invisible
 *
 */

/**
 * Declare a unique namespace for CHAP's Common Hybrid Visualisation Library,
 * "links"
 */
if (typeof links === 'undefined') {
  links = {};
  // important: do not use var, as "var links = {};" will overwrite
  //            the existing links variable value with undefined in IE8, IE7.
}


/**
 * Ensure the variable google exists
 */
if (typeof google === 'undefined') {
  google = undefined;
  // important: do not use var, as "var google = undefined;" will overwrite
  //            the existing google variable value with undefined in IE8, IE7.
}


/**
 * @class Timeline
 * The timeline is a visualization chart to visualize events in time.
 *
 * The timeline is developed in javascript as a Google Visualization Chart.
 *
 * @param {dom_element} container   The DOM element in which the Timeline will
 *                                  be created. Normally a div element.
 */
links.Timeline = function(container) {
  // create variables and set default values
  this.dom = {};
  this.conversion = {};
  this.eventParams = {}; // stores parameters for mouse events
  this.groups = [];
  this.groupIndexes = {};
  this.items = [];
  this.selection = undefined; // stores index and item which is currently selected

  this.listeners = {}; // event listener callbacks

  // Initialize sizes.
  // Needed for IE (which gives an error when you try to set an undefined
  // value in a style)
  this.size = {
    'actualHeight': 0,
    'axis': {
      'characterMajorHeight': 0,
      'characterMajorWidth': 0,
      'characterMinorHeight': 0,
      'characterMinorWidth': 0,
      'height': 0,
      'labelMajorTop': 0,
      'labelMinorTop': 0,
      'line': 0,
      'lineMajorWidth': 0,
      'lineMinorHeight': 0,
      'lineMinorTop': 0,
      'lineMinorWidth': 0,
      'top': 0
    },
    'contentHeight': 0,
    'contentLeft': 0,
    'contentWidth': 0,
    'dataChanged': false,
    'frameHeight': 0,
    'frameWidth': 0,
    'groupsLeft': 0,
    'groupsWidth': 0,
    'items': {
      'top': 0
    }
  };

  this.dom.container = container;

  this.options = {
    'width': "100%",
    'height': "auto",
    'minHeight': 0,       // minimal height in pixels
    'autoHeight': true,

    'eventMargin': 10,    // minimal margin between events
    'eventMarginAxis': 20, // minimal margin beteen events and the axis
    'dragAreaWidth': 10, // pixels

    'moveable': true,
    'zoomable': true,
    'selectable': true,
    'editable': false,
    'snapEvents': true,

    'showCurrentTime': true, // show a red bar displaying the current time
    'showCustomTime': false, // show a blue, draggable bar displaying a custom time
    'showMajorLabels': true,
    'showNavigation': false,
    'showButtonAdd': true,
    'groupsOnRight': false,
    'axisOnTop': false,
    'stackEvents': true,
    'animate': true,
    'animateZoom': true,
    'style': 'box'
  };

  this.clientTimeOffset = 0;    // difference between client time and the time
                                // set via Timeline.setCurrentTime()
  var dom = this.dom;

  // remove all elements from the container element.
  while (dom.container.hasChildNodes()) {
    dom.container.removeChild(dom.container.firstChild);
  }

  // create a step for drawing the axis
  this.step = new links.Timeline.StepDate();

  // initialize data
  this.data = [];
  this.firstDraw = true;

  // date interval must be initialized
  this.setVisibleChartRange(undefined, undefined, false);

  // create all DOM elements
  this.redrawFrame();

  // Internet Explorer does not support Array.indexof,
  // so we define it here in that case
  // http://soledadpenades.com/2007/05/17/arrayindexof-in-internet-explorer/
	if(!Array.prototype.indexOf) {
    Array.prototype.indexOf = function(obj){
      for(var i = 0; i < this.length; i++){
        if(this[i] == obj){
          return i;
        }
      }
      return -1;
    }
	}

  // fire the ready event
  this.trigger('ready');
}


/**
 * Main drawing logic. This is the function that needs to be called
 * in the html page, to draw the timeline.
 *
 * A data table with the events must be provided, and an options table.
 *
 * @param {DataTable}      data    The data containing the events for the timeline.
 *                                 Object DataTable is defined in
 *                                 google.visualization.DataTable
 * @param {name/value map} options A name/value map containing settings for the
 *                                 timeline. Optional.
 */
links.Timeline.prototype.draw = function(data, options) {
  if (options) {
    // retrieve parameter values
    for (var i in options) {
      if (options.hasOwnProperty(i)) {
        this.options[i] = options[i];
      }
    }
  }
  this.options.autoHeight = (this.options.height === "auto");

  // read the data
  this.setData(data);

  // set timer range. this will also redraw the timeline
  if (options && options.start && options.end) {
    this.setVisibleChartRange(options.start, options.end);
  }
  else if (this.firstDraw) {
    this.setVisibleChartRangeAuto();
  }

  this.firstDraw = false;
}

/**
 * Set data for the timeline
 * @param {DataTable or JSON array} data
 */
links.Timeline.prototype.setData = function(data) {
  // unselect any previously selected item
  this.unselectItem();

  if (!data) {
    data = [];
  }

  this.items = [];
  this.data = data;
  var items = this.items;
  var options = this.options;

  // create groups from the data
  this.setGroups(data);

  if (google && google.visualization &&
      data instanceof google.visualization.DataTable) {
    // read DataTable
    var hasGroups = (data.getNumberOfColumns() > 3);
    for (var row = 0, rows = data.getNumberOfRows(); row < rows; row++) {
      items.push(this.createItem({
        'start': data.getValue(row, 0),
        'end': data.getValue(row, 1),
        'content': data.getValue(row, 2),
        'group': (hasGroups ? data.getValue(row, 3) : undefined)
      }));
    }
  }
  else if (links.Timeline.isArray(data)) {
    // read JSON array
    for (var row = 0, rows = data.length; row < rows; row++) {
      var itemData = data[row]
      var item = this.createItem(itemData);
      items.push(item);
    }
  }
  else {
    throw "Unknown data type. DataTable or Array expected.";
  }

  // set a flag to force the recalcSize method to recalculate the
  // heights and widths of the events
  this.size.dataChanged = true;
  this.redrawFrame();      // create the items for the new data
  this.recalcSize();       // position the items
  this.stackEvents(false);
  this.redrawFrame();      // redraw the items on the final positions
  this.size.dataChanged = false;
}

/**
 * Set the groups available in the given dataset
 * @param {DataTable or JSON array} data
 */
links.Timeline.prototype.setGroups = function (data) {
  this.deleteGroups();
  var groups = this.groups;
  var groupIndexes = this.groupIndexes;

  if (google && google.visualization &&
      data instanceof google.visualization.DataTable) {
    // get groups from DataTable
    var hasGroups = (data.getNumberOfColumns() > 3);
    if (hasGroups) {
      var groupNames = data.getDistinctValues(3);
      for (var i = 0, iMax = groupNames.length; i < iMax; i++) {
        this.addGroup(groupNames[i]);
      }
    }
  }
  else if (links.Timeline.isArray(data)){
    // get groups from JSON Array
    for (var i = 0, iMax = data.length; i < iMax; i++) {
      var row = data[i],
        group = row.group;
      if (group) {
        this.addGroup(group);
      }
    }
  }
  else {
    throw 'Unknown data type. DataTable or Array expected.';
  }
}


/**
 * Return the original data table.
 * @param {Google DataTable or Array} data
 */
links.Timeline.prototype.getData = function  () {
  return this.data;
}


/**
 * Update the original data with changed start, end or group.
 *
 * @param {Number} index
 * @param {Object} values   An object containing some of the following parameters:
 *                          {Date} start,
 *                          {Date} end,
 *                          {String} content,
 *                          {String} group
 */
links.Timeline.prototype.updateData = function  (index, values) {
  var data = this.data;

  if (google && google.visualization &&
      data instanceof google.visualization.DataTable) {
    // update the original google DataTable
    var missingRows = (index + 1) - data.getNumberOfRows();
    if (missingRows > 0) {
      data.addRows(missingRows);
    }

    if (values.start) {
      data.setValue(index, 0, values.start);
    }
    if (values.end) {
      data.setValue(index, 1, values.end);
    }
    if (values.content) {
      data.setValue(index, 2, values.content);
    }
    if (values.group && data.getNumberOfColumns() > 3) {
      // TODO: append a column when needed?
      data.setValue(index, 3, values.group);
    }
  }
  else if (links.Timeline.isArray(data)) {
    // update the original JSON table
    var row = data[index];
    if (row == undefined) {
      row = {};
      data[index] = row;
    }

    if (values.start) {
      row.start = values.start;
    }
    if (values.end) {
      row.end = values.end;
    }
    if (values.content) {
      row.content = values.content;
    }
    if (values.group) {
      row.group = values.group;
    }
  }
  else {
    throw "Cannot update data, unknown type of data";
  }
}

/**
 * Find the item index from a given HTML element
 * If no item index is found, undefined is returned
 * @param {HTML DOM element} element
 * @return {Number} index
 */
links.Timeline.prototype.getItemIndex = function(element) {
  var e = element,
    dom = this.dom,
    items = this.items,
    index = undefined;

  // try to find the frame where the items are located in
  while (e.parentNode && e.parentNode !== dom.items.frame) {
    e = e.parentNode;
  }

  if (e.parentNode === dom.items.frame) {
    // yes! we have found the parent element of all items
    // retrieve its id from the array with items
    for (var i = 0, iMax = items.length; i < iMax; i++) {
      if (items[i].dom === e) {
        index = i;
        break;
      }
    }
  }

  return index;
}

/**
 * Set a new size for the timeline
 * @param {string} width   Width in pixels or percentage (for example "800px"
 *                         or "50%")
 * @param {string} height  Height in pixels or percentage  (for example "400px"
 *                         or "30%")
 */
links.Timeline.prototype.setSize = function(width, height) {
  if (width) {
    this.options.width = width;
    this.dom.frame.style.width = width;
  }
  if (height) {
    this.options.height = height;
    this.options.autoHeight = (this.options.height === "auto");
    if (height !==  "auto" ) {
      this.dom.frame.style.height = height;
    }
  }

  this.recalcSize();
  this.stackEvents(false);
  this.redrawFrame();
}


/**
 * Set a new value for the visible range int the timeline.
 * Set start to null to include everything from the earliest date to end.
 * Set end to null to include everything from start to the last date.
 * Example usage:
 *    myTimeline.setVisibleChartRange(new Date("2010-08-22"),
 *                                    new Date("2010-09-13"));
 * @param {Date}   start     The start date for the timeline. optional
 * @param {Date}   end       The end date for the timeline. optional
 * @param {boolean} redraw   Optional. If true (default) the Timeline is
 *                           directly redrawn
 */
links.Timeline.prototype.setVisibleChartRange = function(start, end, redraw) {
  if (start != null) {
    this.start = new Date(start);
  } else {
    // default of 3 days ago
    this.start = new Date();
    this.start.setDate(this.start.getDate() - 3);
  }

  if (end != null) {
    this.end = new Date(end);
  } else {
    // default of 4 days ahead
    this.end = new Date();
    this.end.setDate(this.end.getDate() + 4);
  }

  // prevent start Date <= end Date
  if (this.end.valueOf() <= this.start.valueOf()) {
    this.end = new Date(this.start);
    this.end.setDate(this.end.getDate() + 7);
  }

  if (redraw == undefined || redraw == true) {
    this.recalcSize();
    this.stackEvents(false);
    this.redrawFrame();
  }
  else {
    this.recalcConversion();
  }
}


/**
 * Change the visible chart range such that all items become visible
 */
links.Timeline.prototype.setVisibleChartRangeAuto = function() {
  var items = this.items;
    startMin = undefined, // long value of a data
    endMax = undefined;   // long value of a data

  // find earliest start date from the data
  for (var i = 0, iMax = items.length; i < iMax; i++) {
    var item = items[i],
      start = item.start ? item.start.valueOf() : undefined,
      end = item.end ? item.end.valueOf() : start;

    if (startMin !== undefined && start !== undefined) {
      startMin = Math.min(startMin, start);
    }
    else {
      startMin = start;
    }
    if (endMax !== undefined && end !== undefined) {
      endMax = Math.max(endMax, end);
    }
    else {
      endMax = end;
    }
  }

  if (startMin !== undefined && endMax !== undefined) {
    // zoom out 5% such that you have a little white space on the left and right
    var center = (endMax + startMin) / 2,
      diff = (endMax - startMin);
    startMin = startMin - diff * 0.05;
    endMax = endMax + diff * 0.05;

    // adjust the start and end date
    this.setVisibleChartRange(new Date(startMin), new Date(endMax));
  }
  else {
    this.setVisibleChartRange(undefined, undefined);
  }
}

/**
 * Adjust the visible range such that the current time is located in the center
 * of the timeline
 */
links.Timeline.prototype.setVisibleChartRangeNow = function() {
  var now = new Date();

  var diff = (this.end.getTime() - this.start.getTime());

  var startNew = new Date(now.getTime() - diff/2);
  var endNew = new Date(startNew.getTime() + diff);
  this.setVisibleChartRange(startNew, endNew);
}


/**
 * Retrieve the current visible range in the timeline.
 * @return {Object} An object with start and end properties
 */
links.Timeline.prototype.getVisibleChartRange = function() {
  var range = {
    'start': new Date(this.start),
    'end': new Date(this.end)
  };
  return range;
}


/**
 * Redraw the timeline. This needs to be executed after the start and/or
 * end time are changed, or when data is added or removed dynamically.
 */
links.Timeline.prototype.redrawFrame = function() {
  var dom = this.dom,
    options = this.options,
    size = this.size;

  if (!dom.frame) {
    // the surrounding main frame
    dom.frame = document.createElement("DIV");
    dom.frame.className = "timeline-frame";
    dom.frame.style.position = "relative";
    dom.frame.style.overflow = "hidden";
    dom.container.appendChild(dom.frame);
  }

  if (options.autoHeight) {
    dom.frame.style.height = size.frameHeight + "px";
  }
  else {
    dom.frame.style.height = options.height || "100%";
  }
  dom.frame.style.width = options.width  || "100%";

  this.redrawContent();
  this.redrawGroups();
  this.redrawCurrentTime();
  this.redrawCustomTime();
  this.redrawNavigation();
}


/**
 * Redraw the content of the timeline: the axis and the items
 */
links.Timeline.prototype.redrawContent = function() {
  var dom = this.dom,
    size = this.size;

  if (!dom.content) {
    // create content box where the axis and canvas will
    dom.content = document.createElement("DIV");
    //this.frame.className = "timeline-frame";
    dom.content.style.position = "relative";
    dom.content.style.overflow = "hidden";
    dom.frame.appendChild(dom.content);

    var timelines = document.createElement("DIV");
    timelines.style.position = "absolute";
    timelines.style.left = "0px";
    timelines.style.top = "0px";
    timelines.style.height = "100%";
    timelines.style.width = "0px";
    dom.content.appendChild(timelines);
    dom.contentTimelines = timelines;

    var params = this.eventParams,
      me = this;
    if (!params.onMouseDown) {
      params.onMouseDown = function (event) {me.onMouseDown(event);};
      links.Timeline.addEventListener(dom.content, "mousedown", params.onMouseDown);
    }
    if (!params.onTouchStart) {
      params.onTouchStart = function (event) {me.onTouchStart(event);};
      links.Timeline.addEventListener(dom.content, "touchstart", params.onTouchStart);
    }
    if (!params.onMouseWheel) {
      params.onMouseWheel = function (event) {me.onMouseWheel(event);};
      links.Timeline.addEventListener(dom.content, "mousewheel", params.onMouseWheel);
    }
    if (!params.onDblClick) {
      params.onDblClick = function (event) {me.onDblClick(event);};
      links.Timeline.addEventListener(dom.content, "dblclick", params.onDblClick);
    }
  }
  dom.content.style.left = size.contentLeft + "px";
  dom.content.style.top = "0px";
  dom.content.style.width = size.contentWidth + "px";
  dom.content.style.height = size.frameHeight + "px";

  this.redrawAxis();
  this.redrawItems();
  this.redrawDeleteButton();
  this.redrawDragAreas();
}

/**
 * Redraw the timeline axis with minor and major labels
 */
links.Timeline.prototype.redrawAxis = function() {
  var dom = this.dom,
    options = this.options,
    size = this.size,
    step = this.step;

  var axis = dom.axis;
  if (!axis) {
    axis = {};
    dom.axis = axis;
  }
  if (size.axis.properties === undefined) {
    size.axis.properties = {};
  }
  if (axis.minorTexts === undefined) {
    axis.minorTexts = [];
  }
  if (axis.minorLines === undefined) {
    axis.minorLines = [];
  }
  if (axis.majorTexts === undefined) {
    axis.majorTexts = [];
  }
  if (axis.majorLines === undefined) {
    axis.majorLines = [];
  }

  if (!axis.frame) {
    axis.frame = document.createElement("DIV");
    axis.frame.style.position = "absolute";
    axis.frame.style.left = "0px";
    axis.frame.style.top = "0px";
    dom.content.appendChild(axis.frame);
  }

  // take axis offline
  dom.content.removeChild(axis.frame);

  axis.frame.style.width = (size.contentWidth) + "px";
  axis.frame.style.height = (size.axis.height) + "px";

  // the drawn axis is more wide than the actual visual part, such that
  // the axis can be dragged without having to redraw it each time again.
  var start = this.screenToTime(0);
  var end = this.screenToTime(size.contentWidth);
  var width = size.contentWidth;

  // calculate minimum step (in milliseconds) based on character size
  this.minimumStep = this.screenToTime(size.axis.characterMinorWidth * 6).valueOf() -
                     this.screenToTime(0).valueOf();

  step.setRange(start, end, this.minimumStep);

  this.redrawAxisCharacters();

  this.redrawAxisStartOverwriting();

  step.start();
  var xFirstMajorLabel = undefined;
  while (!step.end()) {
    var cur = step.getCurrent(),
        x = this.timeToScreen(cur),
        isMajor = step.isMajor();

    this.redrawAxisMinorText(x, step.getLabelMinor());

    if (isMajor && options.showMajorLabels) {
      if (x > 0) {
        if (xFirstMajorLabel === undefined) {
          xFirstMajorLabel = x;
        }
        this.redrawAxisMajorText(x, step.getLabelMajor());
      }
      this.redrawAxisMajorLine(x);
    }
    else {
      this.redrawAxisMinorLine(x);
    }

    step.next();
  }

  // create a major label on the left when needed
  if (options.showMajorLabels) {
    var leftTime = this.screenToTime(0),
      leftText = this.step.getLabelMajor(leftTime),
      width = leftText.length * size.axis.characterMajorWidth + 10;// estimation

    if (xFirstMajorLabel === undefined || width < xFirstMajorLabel) {
      this.redrawAxisMajorText(0, leftText, leftTime);
    }
  }

  this.redrawAxisHorizontal();

  // cleanup left over labels
  this.redrawAxisEndOverwriting();

  // put axis online
  dom.content.insertBefore(axis.frame, dom.content.firstChild);

}

/**
 * Create characters used to determine the size of text on the axis
 */
links.Timeline.prototype.redrawAxisCharacters = function () {
  // calculate the width and height of a single character
  // this is used to calculate the step size, and also the positioning of the
  // axis
  var dom = this.dom,
    axis = dom.axis;

  if (!axis.characterMinor) {
    var text = document.createTextNode("0");
    var characterMinor = document.createElement("DIV");
    characterMinor.className = "timeline-axis-text timeline-axis-text-minor";
    characterMinor.appendChild(text);
    characterMinor.style.position = "absolute";
    characterMinor.style.visibility = "hidden";
    characterMinor.style.paddingLeft = "0px";
    characterMinor.style.paddingRight = "0px";
    axis.frame.appendChild(characterMinor);

    axis.characterMinor = characterMinor;
  }

  if (!axis.characterMajor) {
    var text = document.createTextNode("0");
    var characterMajor = document.createElement("DIV");
    characterMajor.className = "timeline-axis-text timeline-axis-text-major";
    characterMajor.appendChild(text);
    characterMajor.style.position = "absolute";
    characterMajor.style.visibility = "hidden";
    characterMajor.style.paddingLeft = "0px";
    characterMajor.style.paddingRight = "0px";
    axis.frame.appendChild(characterMajor);

    axis.characterMajor = characterMajor;
  }
}

/**
 * Initialize redraw of the axis. All existing labels and lines will be
 * overwritten and reused.
 */
links.Timeline.prototype.redrawAxisStartOverwriting = function () {
  var properties = this.size.axis.properties;

  properties.minorTextNum = 0;
  properties.minorLineNum = 0;
  properties.majorTextNum = 0;
  properties.majorLineNum = 0;
}

/**
 * End of overwriting HTML DOM elements of the axis.
 * remaining elements will be removed
 */
links.Timeline.prototype.redrawAxisEndOverwriting = function () {
  var dom = this.dom,
    props = this.size.axis.properties,
    frame = this.dom.axis.frame;

  // remove leftovers
  var minorTexts = dom.axis.minorTexts,
      num = props.minorTextNum;
  while (minorTexts.length > num) {
    var minorText = minorTexts[num];
    frame.removeChild(minorText);
    minorTexts.splice(num, 1);
  }

  var minorLines = dom.axis.minorLines,
      num = props.minorLineNum;
  while (minorLines.length > num) {
    var minorLine = minorLines[num];
    frame.removeChild(minorLine);
    minorLines.splice(num, 1);
  }

  var majorTexts = dom.axis.majorTexts,
      num = props.majorTextNum;
  while (majorTexts.length > num) {
    var majorText = majorTexts[num];
    frame.removeChild(majorText);
    majorTexts.splice(num, 1);
  }

  var majorLines = dom.axis.majorLines,
      num = props.majorLineNum;
  while (majorLines.length > num) {
    var majorLine = majorLines[num];
    frame.removeChild(majorLine);
    majorLines.splice(num, 1);
  }
}

/**
 * Redraw the horizontal line and background of the axis
 */
links.Timeline.prototype.redrawAxisHorizontal = function() {
  var axis = this.dom.axis,
    size = this.size;

  if (!axis.backgroundLine) {
    // create the axis line background (for a background color or so)
    var backgroundLine = document.createElement("DIV");
    backgroundLine.className = "timeline-axis";
    backgroundLine.style.position = "absolute";
    backgroundLine.style.left = "0px";
    backgroundLine.style.width = "100%";
    backgroundLine.style.border = "none";
    axis.frame.insertBefore(backgroundLine, axis.frame.firstChild);

    axis.backgroundLine = backgroundLine;
  }
  axis.backgroundLine.style.top = size.axis.top + "px";
  axis.backgroundLine.style.height = size.axis.height + "px";

  if (axis.line) {
    // put this line at the end of all childs
    var line = axis.frame.removeChild(axis.line);
    axis.frame.appendChild(line);
  }
  else {
    // make the axis line
    var line = document.createElement("DIV");
    line.className = "timeline-axis";
    line.style.position = "absolute";
    line.style.left = "0px";
    line.style.width = "100%";
    line.style.height = "0px";
    axis.frame.appendChild(line);

    axis.line = line;
  }
  axis.line.style.top = size.axis.line + "px";

}

/**
 * Create a minor label for the axis at position x
 * @param {Number} x
 * @param {String} text
 */
links.Timeline.prototype.redrawAxisMinorText = function (x, text) {
  var size = this.size,
      dom = this.dom,
      props = size.axis.properties,
      frame = dom.axis.frame,
      minorTexts = dom.axis.minorTexts,
      index = props.minorTextNum,
      label;

  if (index < minorTexts.length) {
    label = minorTexts[index]
  }
  else {
    // create new label
    var content = document.createTextNode(""),
      label = document.createElement("DIV");
    label.appendChild(content);
    label.className = "timeline-axis-text timeline-axis-text-minor";
    label.style.position = "absolute";

    frame.appendChild(label);

    minorTexts.push(label);
  }

  label.childNodes[0].nodeValue = text;
  label.style.left = x + "px";
  label.style.top  = size.axis.labelMinorTop + "px";
  //label.title = title;  // TODO: this is a heavy operation

  props.minorTextNum++;
}

/**
 * Create a minor line for the axis at position x
 * @param {Number} x
 */
links.Timeline.prototype.redrawAxisMinorLine = function (x) {
  var axis = this.size.axis,
      dom = this.dom,
      props = axis.properties,
      frame = dom.axis.frame,
      minorLines = dom.axis.minorLines,
      index = props.minorLineNum,
      line;

  if (index < minorLines.length) {
    line = minorLines[index];
  }
  else {
    // create vertical line
    line = document.createElement("DIV");
    line.className = "timeline-axis-grid timeline-axis-grid-minor";
    line.style.position = "absolute";
    line.style.width = "0px";

    frame.appendChild(line);
    minorLines.push(line);
  }

  line.style.top = axis.lineMinorTop + "px";
  line.style.height = axis.lineMinorHeight + "px";
  line.style.left = (x - axis.lineMinorWidth/2) + "px";

  props.minorLineNum++;
}

/**
 * Create a Major label for the axis at position x
 * @param {Number} x
 * @param {String} text
 */
links.Timeline.prototype.redrawAxisMajorText = function (x, text) {
  var size = this.size,
      props = size.axis.properties,
      frame = this.dom.axis.frame,
      majorTexts = this.dom.axis.majorTexts,
      index = props.majorTextNum,
      label;

  if (index < majorTexts.length) {
    label = majorTexts[index];
  }
  else {
    // create label
    var content = document.createTextNode(text);
    label = document.createElement("DIV");
    label.className = "timeline-axis-text timeline-axis-text-major";
    label.appendChild(content);
    label.style.position = "absolute";
    label.style.top = "0px";

    frame.appendChild(label);
    majorTexts.push(label);
  }

  label.childNodes[0].nodeValue = text;
  label.style.top = size.axis.labelMajorTop + "px";
  label.style.left = x + "px";
  //label.title = title; // TODO: this is a heavy operation

  props.majorTextNum ++;
}

/**
 * Create a Major line for the axis at position x
 * @param {Number} x
 */
links.Timeline.prototype.redrawAxisMajorLine = function (x) {
  var size = this.size,
      props = size.axis.properties,
      axis = this.size.axis,
      frame = this.dom.axis.frame,
      majorLines = this.dom.axis.majorLines,
      index = props.majorLineNum,
      line;

  if (index < majorLines.length) {
    var line = majorLines[index];
  }
  else {
    // create vertical line
    line = document.createElement("DIV");
    line.className = "timeline-axis-grid timeline-axis-grid-major";
    line.style.position = "absolute";
    line.style.top = "0px";
    line.style.width = "0px";

    frame.appendChild(line);
    majorLines.push(line);
  }

  line.style.left = (x - axis.lineMajorWidth/2) + "px";
  line.style.height = size.frameHeight + "px";

  props.majorLineNum ++;
}

/**
 * Redraw all items
 */
links.Timeline.prototype.redrawItems = function() {
  var dom = this.dom,
    options = this.options,
    boxAlign = (options.box && options.box.align) ? options.box.align : undefined;
    size = this.size,
    contentWidth = size.contentWidth,
    items = this.items;

  if (!dom.items) {
    dom.items = {};
  }

  // draw the frame containing the items
  var frame = dom.items.frame;
  if (!frame) {
    frame = document.createElement("DIV");
    frame.style.position = "relative";
    dom.content.appendChild(frame);
    dom.items.frame = frame;
  }

  frame.style.left = "0px";
  //frame.style.width = "0px";
  frame.style.top = size.items.top + "px";
  frame.style.height = (size.frameHeight - size.axis.height) + "px";

  // initialize arrarys for storing the items
  var ranges = dom.items.ranges;
  if (!ranges) {
    ranges = [];
    dom.items.ranges = ranges;
  }
  var boxes = dom.items.boxes;
  if (!boxes) {
    boxes = [];
    dom.items.boxes = boxes;
  }
  var dots = dom.items.dots;
  if (!dots) {
    dots = [];
    dom.items.dots = dots;
  }

  // Take frame offline
  dom.content.removeChild(frame);

  if (size.dataChanged) {
    // create the items
    var rangesCreated = ranges.length,
      boxesCreated = boxes.length,
      dotsCreated = dots.length,
      rangesUsed = 0,
      boxesUsed = 0,
      dotsUsed = 0,
      itemsLength = items.length;

    for (var i = 0, iMax = items.length; i < iMax; i++) {
      var item = items[i];
      switch (item.type) {
        case 'range':
          if (rangesUsed < rangesCreated) {
            // reuse existing range
            var domItem = ranges[rangesUsed];
            domItem.firstChild.innerHTML = item.content;
            domItem.style.display = '';
            item.dom = domItem;
            rangesUsed++;
          }
          else {
            // create a new range
            var domItem = this.createEventRange(item.content);
            ranges[rangesUsed] = domItem;
            frame.appendChild(domItem);
            item.dom = domItem;
            rangesUsed++;
            rangesCreated++;
          }
          break;

        case 'box':
          if (boxesUsed < boxesCreated) {
            // reuse existing box
            var domItem = boxes[boxesUsed];
            domItem.firstChild.innerHTML = item.content;
            domItem.style.display = '';
            item.dom = domItem;
            boxesUsed++;
          }
          else {
            // create a new box
            var domItem = this.createEventBox(item.content);
            boxes[boxesUsed] = domItem;
            frame.appendChild(domItem);
            frame.insertBefore(domItem.line, frame.firstChild);
            // Note: line must be added in front of the items,
            //       such that it stays below all items
            frame.appendChild(domItem.dot);
            item.dom = domItem;
            boxesUsed++;
            boxesCreated++;
          }
          break;

        case 'dot':
          if (dotsUsed < dotsCreated) {
            // reuse existing box
            var domItem = dots[dotsUsed];
            domItem.firstChild.innerHTML = item.content;
            domItem.style.display = '';
            item.dom = domItem;
            dotsUsed++;
          }
          else {
            // create a new box
            var domItem = this.createEventDot(item.content);
            dots[dotsUsed] = domItem;
            frame.appendChild(domItem);
            item.dom = domItem;
            dotsUsed++;
            dotsCreated++;
          }
          break;

        default:
          // do nothing
          break;
      }
    }

    // remove redundant items when needed
    for (var i = rangesUsed; i < rangesCreated; i++) {
      frame.removeChild(ranges[i]);
    }
    ranges.splice(rangesUsed, rangesCreated - rangesUsed);
    for (var i = boxesUsed; i < boxesCreated; i++) {
      var box = boxes[i];
      frame.removeChild(box.line);
      frame.removeChild(box.dot);
      frame.removeChild(box);
    }
    boxes.splice(boxesUsed, boxesCreated - boxesUsed);
    for (var i = dotsUsed; i < dotsCreated; i++) {
      frame.removeChild(dots[i]);
    }
    dots.splice(dotsUsed, dotsCreated - dotsUsed);
  }

  // reposition all items
  for (var i = 0, iMax = items.length; i < iMax; i++) {
    var item = items[i],
      domItem = item.dom;

    switch (item.type) {
      case 'range':
        var left = this.timeToScreen(item.start),
          right = this.timeToScreen(item.end);

        // limit the width of the item, as browsers cannot draw very wide divs
        if (left < -contentWidth) {
          left = -contentWidth;
        }
        if (right > 2 * contentWidth) {
          right = 2 * contentWidth;
        }

        var visible = right > -contentWidth && left < 2 * contentWidth;
        if (visible || size.dataChanged) {
          // when data is changed, all items must be kept visible, as their heights must be measured
          if (item.hidden) {
            item.hidden = false;
            domItem.style.display = '';
          }
          domItem.style.top = item.top + "px";
          domItem.style.left = left + "px";
          //domItem.style.width = Math.max(right - left - 2 * item.borderWidth, 1) + "px"; // TODO: borderWidth
          domItem.style.width = Math.max(right - left, 1) + "px";
        }
        else {
          // hide when outside of the current window
          if (!item.hidden) {
            domItem.style.display = 'none';
            item.hidden = true;
          }
        }

        break;

      case 'box':
        var left = this.timeToScreen(item.start);

        var axisOnTop = options.axisOnTop,
          axisHeight = size.axis.height,
          axisTop = size.axis.top;
        var visible = ((left + item.width/2 > -contentWidth) &&
          (left - item.width/2 < 2 * contentWidth));
        if (visible || size.dataChanged) {
          // when data is changed, all items must be kept visible, as their heights must be measured
          if (item.hidden) {
            item.hidden = false;
            domItem.style.display = '';
            domItem.line.style.display = '';
            domItem.dot.style.display = '';
          }
          domItem.style.top = item.top + "px";
          if (boxAlign == 'right') {
            domItem.style.left = (left - item.width) + "px";
          }
          else if (boxAlign == 'left') {
            domItem.style.left = (left) + "px";
          }
          else { // default or 'center'
            domItem.style.left = (left - item.width/2) + "px";
          }

          var line = domItem.line;
          line.style.left = (left - item.lineWidth/2) + "px";
          if (axisOnTop) {
            //line.style.top = axisHeight + "px"; // TODO: cleanup
            //line.style.height = (item.top - axisHeight) + "px";
            line.style.top = "0px";
            line.style.height = Math.max(item.top, 0) + "px";
          }
          else {
            line.style.top = (item.top + item.height) + "px";
            line.style.height = Math.max(axisTop - item.top - item.height, 0) + "px";
          }

          var dot = domItem.dot;
          dot.style.left = (left - item.dotWidth/2) + "px";
          dot.style.top = (axisTop - item.dotHeight/2) + "px";
        }
        else {
          // hide when outside of the current window
          if (!item.hidden) {
            domItem.style.display = 'none';
            domItem.line.style.display = 'none';
            domItem.dot.style.display = 'none';
            item.hidden = true;
          }
        }
        break;

      case 'dot':
        var left = this.timeToScreen(item.start);

        var axisOnTop = options.axisOnTop,
          axisHeight = size.axis.height,
          axisTop = size.axis.top;
        var visible = (left + item.width > -contentWidth) && (left < 2 * contentWidth);
        if (visible || size.dataChanged) {
          // when data is changed, all items must be kept visible, as their heights must be measured
          if (item.hidden) {
            item.hidden = false;
            domItem.style.display = '';
          }
          domItem.style.top = item.top + "px";
          domItem.style.left = (left - item.dotWidth / 2) + "px";

          domItem.content.style.marginLeft = (1.5 * item.dotWidth) + "px";
          //domItem.content.style.marginRight = (0.5 * item.dotWidth) + "px"; // TODO
          domItem.dot.style.top = ((item.height - item.dotHeight) / 2) + "px";
        }
        else {
          // hide when outside of the current window
          if (!item.hidden) {
            domItem.style.display = 'none';
            item.hidden = true;
          }
        }
        break;

      default:
        // do nothing
        break;
    }
  }

  // move selected item to the end, to ensure that it is always on top
  if (this.selection) {
    var item = this.selection.item;
    frame.removeChild(item);
    frame.appendChild(item);
  }

  // put frame online again
  dom.content.appendChild(frame);

  /* TODO
  // retrieve all image sources from the items, and set a callback once
  // all images are retrieved
  var urls = [];
  var timeline = this;
  links.Timeline.filterImageUrls(frame, urls);
  if (urls.length) {
    for (var i = 0; i < urls.length; i++) {
      var url = urls[i];
      var callback = function (url) {
        timeline.redraw();
      };
      var sendCallbackWhenAlreadyLoaded = false;
      links.imageloader.load(url, callback, sendCallbackWhenAlreadyLoaded);
    }
  }
  */
}


/**
 * Create an event in the timeline, with (optional) formatting: inside a box
 * with rounded corners, and a vertical line+dot to the axis.
 * @param {string} content    The content for the event. This can be plain text
 *                            or HTML code.
 */
links.Timeline.prototype.createEventBox = function(content) {
  // background box
  var divBox = document.createElement("DIV");
  divBox.style.position = "absolute";
  divBox.style.left  = "0px";
  divBox.style.top = "0px";
  divBox.className  = "timeline-event timeline-event-box";

  // contents box (inside the background box). used for making margins
  var divContent = document.createElement("DIV");
  divContent.className = "timeline-event-content";
  divContent.innerHTML = content;
  divBox.appendChild(divContent);

  // line to axis
  var divLine = document.createElement("DIV");
  divLine.style.position = "absolute";
  divLine.style.width = "0px";
  divLine.className = "timeline-event timeline-event-line";
  // important: the vertical line is added at the front of the list of elements,
  // so it will be drawn behind all boxes and ranges
  divBox.line = divLine;

  // dot on axis
  var divDot = document.createElement("DIV");
  divDot.style.position = "absolute";
  divDot.style.width  = "0px";
  divDot.style.height = "0px";
  divDot.className  = "timeline-event timeline-event-dot";
  divBox.dot = divDot;

  return divBox;
}


/**
 * Create an event in the timeline: a dot, followed by the content.
 * @param {string} content    The content for the event. This can be plain text
 *                            or HTML code.
 */
links.Timeline.prototype.createEventDot = function(content) {
  // background box
  var divBox = document.createElement("DIV");
  divBox.style.position = "absolute";

  // contents box, right from the dot
  var divContent = document.createElement("DIV");
  divContent.className = "timeline-event-content";
  divContent.innerHTML = content;
  divBox.appendChild(divContent);

  // dot at start
  var divDot = document.createElement("DIV");
  divDot.style.position = "absolute";
  divDot.className = "timeline-event timeline-event-dot";
  divDot.style.width = "0px";
  divDot.style.height = "0px";
  divBox.appendChild(divDot);

  divBox.content = divContent;
  divBox.dot = divDot;

  return divBox;
}


/**
 * Create an event range as a beam in the timeline.
 * @param {string}  content    The content for the event. This can be plain text
 *                             or HTML code.
 */
links.Timeline.prototype.createEventRange = function(content) {
  // background box
  var divBox = document.createElement("DIV");
  divBox.style.position = "absolute";
  divBox.className = "timeline-event timeline-event-range";

  // contents box
  var divContent = document.createElement("DIV");
  divContent.className = "timeline-event-content";
  divContent.innerHTML = content;
  divBox.appendChild(divContent);

  return divBox;
}

/**
 * Redraw the group labels
 */
links.Timeline.prototype.redrawGroups = function() {
  var dom = this.dom,
    options = this.options,
    size = this.size,
    groups = this.groups;

  if (dom.groups === undefined) {
    dom.groups = {};
  }

  var labels = dom.groups.labels;
  if (!labels) {
    labels = [];
    dom.groups.labels = labels;
  }
  var labelLines = dom.groups.labelLines;
  if (!labelLines) {
    labelLines = [];
    dom.groups.labelLines = labelLines;
  }
  var itemLines = dom.groups.itemLines;
  if (!itemLines) {
    itemLines = [];
    dom.groups.itemLines = itemLines;
  }

  // create the frame for holding the groups
  var frame = dom.groups.frame;
  if (!frame) {
    var frame =  document.createElement("DIV");
    frame.className = "timeline-groups-axis";
    frame.style.position = "absolute";
    frame.style.overflow = "hidden";
    frame.style.top = "0px";
    frame.style.height = "100%";

    dom.frame.appendChild(frame);
    dom.groups.frame = frame;
  }

  frame.style.left = size.groupsLeft + "px";
  frame.style.width = (options.groupsWidth !== undefined) ?
    options.groupsWidth :
    size.groupsWidth + "px";

  // hide groups axis when there are no groups
  if (groups.length == 0) {
    frame.style.display = 'none';
  }
  else {
    frame.style.display = '';
  }

  if (size.dataChanged) {
    // create the items
    var current = labels.length,
      needed = groups.length;

    // overwrite existing items
    for (var i = 0, iMax = Math.min(current, needed); i < iMax; i++) {
      var group = groups[i];
      var label = labels[i];
      label.innerHTML = group.content;
      label.style.display = '';
    }

    // append new items when needed
    for (var i = current; i < needed; i++) {
      var group = groups[i];

      // create text label
      var label = document.createElement("DIV");
      label.className = "timeline-groups-text";
      label.style.position = "absolute";
      if (options.groupsWidth === undefined) {
        label.style.whiteSpace = "nowrap";
      }
      label.innerHTML = group.content;
      frame.appendChild(label);
      labels[i] = label;

      // create the grid line between the group labels
      var labelLine = document.createElement("DIV");
      labelLine.className = "timeline-axis-grid timeline-axis-grid-minor";
      labelLine.style.position = "absolute";
      labelLine.style.left = "0px";
      labelLine.style.width = "100%";
      labelLine.style.height = "0px";
      labelLine.style.borderTopStyle = "solid";
      frame.appendChild(labelLine);
      labelLines[i] = labelLine;

      // create the grid line between the items
      var itemLine = document.createElement("DIV");
      itemLine.className = "timeline-axis-grid timeline-axis-grid-minor";
      itemLine.style.position = "absolute";
      itemLine.style.left = "0px";
      itemLine.style.width = "100%";
      itemLine.style.height = "0px";
      itemLine.style.borderTopStyle = "solid";
      dom.content.insertBefore(itemLine, dom.content.firstChild);
      itemLines[i] = itemLine;
    }

    // remove redundant items from the DOM when needed
    for (var i = needed; i < current; i++) {
      var label = labels[i],
        labelLine = labelLines[i],
        itemLine = itemLines[i];

      frame.removeChild(label);
      frame.removeChild(labelLine);
      dom.content.removeChild(itemLine);
    }
    labels.splice(needed, current - needed);
    labelLines.splice(needed, current - needed);
    itemLines.splice(needed, current - needed);

    frame.style.borderStyle = options.groupsOnRight ?
      "none none none solid" :
      "none solid none none";
  }

  // position the groups
  for (var i = 0, iMax = groups.length; i < iMax; i++) {
    var group = groups[i],
      label = labels[i],
      labelLine = labelLines[i],
      itemLine = itemLines[i];

    label.style.top = group.labelTop + "px";
    labelLine.style.top = group.lineTop + "px";
    itemLine.style.top = group.lineTop + "px";
    itemLine.style.width = size.contentWidth + "px";
  }

  if (!dom.groups.background) {
    // create the axis grid line background
    var background = document.createElement("DIV");
    background.className = "timeline-axis";
    background.style.position = "absolute";
    background.style.left = "0px";
    background.style.width = "100%";
    background.style.border = "none";

    frame.appendChild(background);
    dom.groups.background = background;
  }
  dom.groups.background.style.top = size.axis.top + 'px';
  dom.groups.background.style.height = size.axis.height + 'px';

  if (!dom.groups.line) {
    // create the axis grid line
    var line = document.createElement("DIV");
    line.className = "timeline-axis";
    line.style.position = "absolute";
    line.style.left = "0px";
    line.style.width = "100%";
    line.style.height = "0px";

    frame.appendChild(line);
    dom.groups.line = line;
  }
  dom.groups.line.style.top = size.axis.line + 'px';
}


/**
 * Redraw the current time bar
 */
links.Timeline.prototype.redrawCurrentTime = function() {
  var options = this.options,
    dom = this.dom,
    size = this.size;

  if (!options.showCurrentTime) {
    if (dom.currentTime) {
      dom.contentTimelines.removeChild(dom.currentTime);
      delete dom.currentTime;
    }

    return;
  }

  if (!dom.currentTime) {
    // create the current time bar
    var currentTime = document.createElement("DIV");
    currentTime.className = "timeline-currenttime";
    currentTime.style.position = "absolute";
    currentTime.style.top = "0px";
    currentTime.style.height = "100%";

    dom.contentTimelines.appendChild(currentTime);
    dom.currentTime = currentTime;
  }

  var now = new Date();
  var nowOffset = new Date(now.getTime() + this.clientTimeOffset);
  var x = this.timeToScreen(nowOffset);

  var visible = (x > -size.contentWidth && x < 2 * size.contentWidth);
  dom.currentTime.style.display = visible ? '' : 'none';
  dom.currentTime.style.left = x + "px";
  dom.currentTime.title = "Current time: " + nowOffset;

  // start a timer to adjust for the new time
  if (this.currentTimeTimer != undefined) {
    clearTimeout(this.currentTimeTimer);
    delete this.currentTimeTimer;
  }
  var timeline = this;
  var onTimeout = function() {
    timeline.redrawCurrentTime();
  }
  // the time equal to the width of one pixel, divided by 2 for more smoothness
  var interval = 1 / this.conversion.factor / 2;
  if (interval < 30) interval = 30;
  this.currentTimeTimer = setTimeout(onTimeout, interval);
}

/**
 * Redraw the custom time bar
 */
links.Timeline.prototype.redrawCustomTime = function() {
  var options = this.options,
    dom = this.dom,
    size = this.size;

  if (!options.showCustomTime) {
    if (dom.customTime) {
      dom.contentTimelines.removeChild(dom.customTime);
      delete dom.customTime;
    }

    return;
  }

  if (!dom.customTime) {
    var customTime = document.createElement("DIV");
    customTime.className = "timeline-customtime";
    customTime.style.position = "absolute";
    customTime.style.top = "0px";
    customTime.style.height = "100%";

    var drag = document.createElement("DIV");
    drag.style.position = "relative";
    drag.style.top = "0px";
    drag.style.left = "-10px";
    drag.style.height = "100%";
    drag.style.width = "20px";
    customTime.appendChild(drag);

    dom.contentTimelines.appendChild(customTime);
    dom.customTime = customTime;

    // initialize parameter
    this.customTime = new Date();
  }

  var x = this.timeToScreen(this.customTime),
    visible = (x > -size.contentWidth && x < 2 * size.contentWidth);
  dom.customTime.style.display = visible ? '' : 'none';
  dom.customTime.style.left = x + "px";
  dom.customTime.title = "Time: " + this.customTime;
}


/**
 * Redraw the delete button, on the top right of the currently selected item
 * if there is no item selected, the button is hidden.
 */
links.Timeline.prototype.redrawDeleteButton = function () {
  var timeline = this,
    options = this.options,
    dom = this.dom,
    size = this.size,
    frame = dom.items.frame;

  if (!options.editable) {
    return;
  }

  var deleteButton = dom.items.deleteButton;
  if (!deleteButton) {
    // create a delete button
    deleteButton = document.createElement("DIV");
    deleteButton.className = "timeline-navigation-delete";
    deleteButton.style.position = "absolute";

    frame.appendChild(deleteButton);
    dom.items.deleteButton = deleteButton;
  }

  if (this.selection) {
    var index = this.selection.index,
      item = this.items[index],
      domItem = this.selection.item,
      right,
      top = item.top;

    switch (item.type) {
      case 'range':
        right = this.timeToScreen(item.end);
        break;

      case 'box':
        //right = this.timeToScreen(item.start) + item.width / 2 + item.borderWidth; // TODO: borderWidth
        right = this.timeToScreen(item.start) + item.width / 2;
        break;

      case 'dot':
        right = this.timeToScreen(item.start) + item.width;
        break;
    }

    // limit the position
    if (right < -size.contentWidth) {
      right = -size.contentWidth;
    }
    if (right > 2 * size.contentWidth) {
      right = 2 * size.contentWidth;
    }

    deleteButton.style.left = right + 'px';
    deleteButton.style.top = top + 'px';
    deleteButton.style.display = '';
    frame.removeChild(deleteButton);
    frame.appendChild(deleteButton);
  }
  else {
    deleteButton.style.display = 'none';
  }
}


/**
 * Redraw the drag areas. When an item (ranges only) is selected,
 * it gets a drag area on the left and right side, to change its width
 */
links.Timeline.prototype.redrawDragAreas = function () {
  var timeline = this,
    options = this.options,
    dom = this.dom,
    size = this.size,
    frame = this.dom.items.frame;

  if (!options.editable) {
    return;
  }

  // create left drag area
  var dragLeft = dom.items.dragLeft;
  if (!dragLeft) {
    dragLeft = document.createElement("DIV");
    dragLeft.style.width = options.dragAreaWidth + "px";
    dragLeft.style.position = "absolute";
    dragLeft.style.cursor = "w-resize";

    frame.appendChild(dragLeft);
    dom.items.dragLeft = dragLeft;
  }

  // create right drag area
  var dragRight = dom.items.dragRight;
  if (!dragRight) {
    dragRight = document.createElement("DIV");
    dragRight.style.width = options.dragAreaWidth + "px";
    dragRight.style.position = "absolute";
    dragRight.style.cursor = "e-resize";

    frame.appendChild(dragRight);
    dom.items.dragRight = dragRight;
  }

  // reposition left and right drag area
  if (this.selection) {
    var index = this.selection.index,
      item = this.items[index];

    if (item.type == 'range') {
      var domItem = item.dom,
      left = this.timeToScreen(item.start),
      right = this.timeToScreen(item.end),
      top = item.top,
      height = item.height;

      dragLeft.style.left = left + 'px';
      dragLeft.style.top = top + 'px';
      dragLeft.style.height = height + 'px';
      dragLeft.style.display = '';
      frame.removeChild(dragLeft);
      frame.appendChild(dragLeft);

      dragRight.style.left = (right - options.dragAreaWidth) + 'px';
      dragRight.style.top = top + 'px';
      dragRight.style.height = height + 'px';
      dragRight.style.display = '';
      frame.removeChild(dragRight);
      frame.appendChild(dragRight);
    }
  }
  else {
    dragLeft.style.display = 'none';
    dragRight.style.display = 'none';
  }
}



/**
 * Create the navigation buttons for zooming and moving
 */
links.Timeline.prototype.redrawNavigation = function () {
  var timeline = this,
    options = this.options,
    dom = this.dom,
    frame = dom.frame,
    navBar = dom.navBar;

  if (!navBar) {
    if (options.editable || options.showNavigation) {
      // create a navigation bar containing the navigation buttons
      navBar = document.createElement("DIV");
      navBar.style.position = "absolute";
      navBar.className = "timeline-navigation";
      if (options.groupsOnRight) {
        navBar.style.left = '10px';
      }
      else {
        navBar.style.right = '10px';
      }
      if (options.axisOnTop) {
        navBar.style.bottom = '10px';
      }
      else {
        navBar.style.top = '10px';
      }
      dom.navBar = navBar;
      frame.appendChild(navBar);
    }

    if (options.editable && options.showButtonAdd) {
      // create a new in button
      navBar.addButton = document.createElement("DIV");
      navBar.addButton.className = "timeline-navigation-new";

      navBar.addButton.title = "Create new event";
      var onAdd = function(event) {
        links.Timeline.preventDefault(event);
        links.Timeline.stopPropagation(event);

        // create a new event at the center of the frame
        var w = timeline.size.contentWidth;
        var x = w / 2;
        var xstart = timeline.screenToTime(x - w / 10); // subtract 10% of timeline width
        var xend = timeline.screenToTime(x + w / 10); // add 10% of timeline width
        if (options.snapEvents) {
          timeline.step.snap(xstart);
          timeline.step.snap(xend);
        }

        var content = "New";
        var group = timeline.groups.length ? timeline.groups[0].content : undefined;

        timeline.addItem({
          'start': xstart,
          'end': xend,
          'content': content,
          'group': group
        });
        var index = (timeline.items.length - 1);
        timeline.selectItem(index);

        timeline.applyAdd = true;

        // fire an add event.
        // Note that the change can be canceled from within an event listener if
        // this listener calls the method cancelAdd().
        timeline.trigger('add');

        if (!timeline.applyAdd) {
          // undo an add
          timeline.deleteItem(index);
        }
        timeline.redrawDeleteButton();
        timeline.redrawDragAreas();
      };
      links.Timeline.addEventListener(navBar.addButton, "mousedown", onAdd);
      navBar.appendChild(navBar.addButton);
    }

    if (options.editable && options.showButtonAdd && options.showNavigation) {
      // create a separator line
      navBar.addButton.style.borderRightWidth = "1px";
      navBar.addButton.style.borderRightStyle = "solid";
    }

    if (options.showNavigation) {
      // create a zoom in button
      navBar.zoomInButton = document.createElement("DIV");
      navBar.zoomInButton.className = "timeline-navigation-zoom-in";
      navBar.zoomInButton.title = "Zoom in";
      var onZoomIn = function(event) {
        links.Timeline.preventDefault(event);
        links.Timeline.stopPropagation(event);
        timeline.zoom(0.4);
        timeline.trigger("rangechange");
        timeline.trigger("rangechanged");
      };
      links.Timeline.addEventListener(navBar.zoomInButton, "mousedown", onZoomIn);
      navBar.appendChild(navBar.zoomInButton);

      // create a zoom out button
      navBar.zoomOutButton = document.createElement("DIV");
      navBar.zoomOutButton.className = "timeline-navigation-zoom-out";
      navBar.zoomOutButton.title = "Zoom out";
      var onZoomOut = function(event) {
        links.Timeline.preventDefault(event);
        links.Timeline.stopPropagation(event);
        timeline.zoom(-0.4);
        timeline.trigger("rangechange");
        timeline.trigger("rangechanged");
      };
      links.Timeline.addEventListener(navBar.zoomOutButton, "mousedown", onZoomOut);
      navBar.appendChild(navBar.zoomOutButton);

      // create a move left button
      navBar.moveLeftButton = document.createElement("DIV");
      navBar.moveLeftButton.className = "timeline-navigation-move-left";
      navBar.moveLeftButton.title = "Move left";
      var onMoveLeft = function(event) {
        links.Timeline.preventDefault(event);
        links.Timeline.stopPropagation(event);
        timeline.move(-0.2);
        timeline.trigger("rangechange");
        timeline.trigger("rangechanged");
      };
      links.Timeline.addEventListener(navBar.moveLeftButton, "mousedown", onMoveLeft);
      navBar.appendChild(navBar.moveLeftButton);

      // create a move right button
      navBar.moveRightButton = document.createElement("DIV");
      navBar.moveRightButton.className = "timeline-navigation-move-right";
      navBar.moveRightButton.title = "Move right";
      var onMoveRight = function(event) {
        links.Timeline.preventDefault(event);
        links.Timeline.stopPropagation(event);
        timeline.move(0.2);
        timeline.trigger("rangechange");
        timeline.trigger("rangechanged");
      };
      links.Timeline.addEventListener(navBar.moveRightButton, "mousedown", onMoveRight);
      navBar.appendChild(navBar.moveRightButton);
    }
  }
}


/**
 * Set current time. This function can be used to set the time in the client
 * timeline equal with the time on a server.
 * @param {Date} time
 */
links.Timeline.prototype.setCurrentTime = function(time) {
  var now = new Date();
  this.clientTimeOffset = time.getTime() - now.getTime();

  this.redrawCurrentTime();
}

/**
 * Get current time. The time can have an offset from the real time, when
 * the current time has been changed via the method setCurrentTime.
 * @return {Date} time
 */
links.Timeline.prototype.getCurrentTime = function() {
  var now = new Date();
  return new Date(now.getTime() + this.clientTimeOffset);
}


/**
 * Set custom time.
 * The custom time bar can be used to display events in past or future.
 * @param {Date} time
 */
links.Timeline.prototype.setCustomTime = function(time) {
  this.customTime = new Date(time);
  this.redrawCustomTime();
}

/**
 * Retrieve the current custom time.
 * @return {Date} customTime
 */
links.Timeline.prototype.getCustomTime = function() {
  return new Date(this.customTime);
}

/**
 * Set a custom scale. Autoscaling will be disabled.
 * For example setScale(SCALE.MINUTES, 5) will result
 * in minor steps of 5 minutes, and major steps of an hour.
 *
 * @param {Step.SCALE} newScale  A scale. Choose from SCALE.MILLISECOND,
 *                               SCALE.SECOND, SCALE.MINUTE, SCALE.HOUR,
 *                               SCALE.DAY, SCALE.MONTH, SCALE.YEAR.
 * @param {int}        newStep   A step size, by default 1. Choose for
 *                               example 1, 2, 5, or 10.
 */
links.Timeline.prototype.setScale = function(scale, step) {
  this.step.setScale(scale, step);
  this.redrawFrame();
}

/**
 * Enable or disable autoscaling
 * @param {boolean} enable  If true or not defined, autoscaling is enabled.
 *                          If false, autoscaling is disabled.
 */
links.Timeline.prototype.setAutoScale = function(enable) {
  this.step.setAutoScale(enable);
  this.redrawFrame();
}

/**
 * Redraw the timeline
 * Reloads the (linked) data table and redraws the timeline when resized.
 * See also the method checkResize
 */
links.Timeline.prototype.redraw = function() {
  this.setData(this.data);
}


/**
 * Check if the timeline is resized, and if so, redraw the timeline.
 * Useful when the webpage is resized.
 */
links.Timeline.prototype.checkResize = function() {
  var resized = this.recalcSize();
  if (resized) {
    this.redrawFrame();
  }
}

/**
 * Recursively retrieve all image urls from the images located inside a given
 * HTML element
 * @param {HTMLElement} elem
 * @param {Array with String} urls   Urls will be added here (no duplicates)
 */
links.Timeline.filterImageUrls = function(elem, urls) {
  var child = elem.firstChild;
  while (child) {
    if (child.tagName == 'IMG') {
      var url = child.src;
      if (urls.indexOf(url) == -1) {
        urls.push(url);
      }
    }

    links.Timeline.filterImageUrls(child, urls);

    child = child.nextSibling;
  }
}

/**
 * Recalculate the sizes of all frames, groups, items, axis
 * After recalcSize() is executed, the Timeline should be redrawn normally
 *
 * @return {boolean} resized   Returns true when the timeline has been resized
 */
links.Timeline.prototype.recalcSize = function() {
  var resized = false;

  var timeline = this;
    size = this.size,
    options = this.options,
    axisOnTop = options.axisOnTop,
    dom = this.dom,
    axis = dom.axis,
    groups = this.groups,
    labels = dom.groups.labels,
    items = this.items

    groupsWidth = size.groupsWidth,
    characterMinorWidth  = axis.characterMinor ? axis.characterMinor.clientWidth : 0,
    characterMinorHeight = axis.characterMinor ? axis.characterMinor.clientHeight : 0,
    characterMajorWidth  = axis.characterMajor ? axis.characterMajor.clientWidth : 0,
    characterMajorHeight = axis.characterMajor ? axis.characterMajor.clientHeight : 0,
    axisHeight = characterMinorHeight + (options.showMajorLabels ? characterMajorHeight : 0),
    actualHeight = size.actualHeight || axisHeight;

  // TODO: move checking for loaded items when creating the dom
  if (size.dataChanged) {
    // retrieve all image sources from the items, and set a callback once
    // all images are retrieved
    var urls = [];
    for (var i = 0, iMax = items.length; i < iMax; i++) {
      var item = items[i],
        domItem = item.dom;

      if (domItem) {
        links.Timeline.filterImageUrls(domItem, urls);
      }
    }
    if (urls.length) {
      for (var i = 0; i < urls.length; i++) {
        var url = urls[i];
        var callback = function (url) {
          timeline.redraw();
        };
        var sendCallbackWhenAlreadyLoaded = false;
        links.imageloader.load(url, callback, sendCallbackWhenAlreadyLoaded);
      }
    }
  }

  // check sizes of the items and groups (width and height) when the data is changed
  if (size.dataChanged) { // TODO: always calculate the size of an item?
  //if (true) {
    groupsWidth = 0;

    // loop through all groups to get the maximum width and the heights
    for (var i = 0, iMax = labels.length; i < iMax; i++) {
      var group = groups[i];
      group.width = labels[i].clientWidth;
      group.height = labels[i].clientHeight;
      group.labelHeight = group.height;

      groupsWidth = Math.max(groupsWidth, group.width);
    }

    // loop through the width and height of all items
    for (var i = 0, iMax = items.length; i < iMax; i++) {
      var item = items[i],
        domItem = item.dom,
        group = item.group;

      var width = domItem ? domItem.clientWidth : 0;
      var height = domItem ? domItem.clientHeight : 0;
      resized = resized || (item.width != width);
      resized = resized || (item.height != height);
      item.width = width;
      item.height = height;
      //item.borderWidth = (domItem.offsetWidth - domItem.clientWidth - 2) / 2; // TODO: borderWidth

      switch (item.type) {
        case 'range':
          break;

        case 'box':
          item.dotHeight = domItem.dot.offsetHeight;
          item.dotWidth = domItem.dot.offsetWidth;
          item.lineWidth = domItem.line.offsetWidth;
          break;

        case 'dot':
          item.dotHeight = domItem.dot.offsetHeight;
          item.dotWidth = domItem.dot.offsetWidth;
          item.contentHeight = domItem.content.offsetHeight;
          break;
      }

      if (group) {
        group.height = group.height ? Math.max(group.height, item.height) : item.height;
      }
    }

    // calculate the actual height of the timeline (needed for auto sizing
    // the timeline)
    actualHeight = axisHeight + 2 * options.eventMarginAxis;
    for (var i = 0, iMax = groups.length; i < iMax; i++) {
      actualHeight += groups[i].height + options.eventMargin;
    }
  }

  // calculate actual height of the timeline when there are no groups
  // but stacked items
  if (groups.length == 0 && options.autoHeight) {
    var min = 0,
      max = 0;

    if (this.animation && this.animation.finalItems) {
      // adjust the offset of all finalItems when the actualHeight has been changed
      var finalItems = this.animation.finalItems,
        finalItem = finalItems[0];
      if (finalItem && finalItem.top) {
        min = finalItem.top,
        max = finalItem.top + finalItem.height;
      }
      for (var i = 1, iMax = finalItems.length; i < iMax; i++) {
        finalItem = finalItems[i];
        min = Math.min(min, finalItem.top);
        max = Math.max(max, finalItem.top + finalItem.height);
      }
    }
    else {
      var item = items[0];
      if (item && item.top) {
        min = item.top,
        max = item.top + item.height;
      }
      for (var i = 1, iMax = items.length; i < iMax; i++) {
        var item = items[i];
        if (item.top) {
          min = Math.min(min, item.top);
          max = Math.max(max, (item.top + item.height));
        }
      }
    }

    actualHeight = (max - min) + 2 * options.eventMarginAxis + axisHeight;

    if (size.actualHeight != actualHeight && options.autoHeight && !options.axisOnTop) {
      // adjust the offset of all items when the actualHeight has been changed
      var diff = actualHeight - size.actualHeight;
      if (this.animation && this.animation.finalItems) {
        var finalItems = this.animation.finalItems;
        for (var i = 0, iMax = finalItems.length; i < iMax; i++) {
          finalItems[i].top += diff;
          finalItems[i].item.top += diff; // TODO
        }
      }
      else {
        for (var i = 0, iMax = items.length; i < iMax; i++) {
          items[i].top += diff;
        }
      }
    }
  }

  // now the heights of the elements are known, we can calculate the the
  // width and height of frame and axis and content
  // Note: IE7 has issues with giving frame.clientWidth, therefore I use offsetWidth instead
  var frameWidth  = dom.frame ? dom.frame.offsetWidth : 0,
    frameHeight = Math.max(options.autoHeight ?
      actualHeight : (dom.frame ? dom.frame.clientHeight : 0),
      options.minHeight),
    axisTop  = axisOnTop ? 0 : frameHeight - axisHeight,
    axisLine = axisOnTop ? axisHeight : axisTop,
    itemsTop = axisOnTop ? axisHeight : 0,
    contentHeight = Math.max(frameHeight - axisHeight, 0);

  if (options.groupsWidth !== undefined) {
    groupsWidth = dom.groups.frame ? dom.groups.frame.clientWidth : 0;
  }
  var groupsLeft = options.groupsOnRight ? frameWidth - groupsWidth : 0;

  if (size.dataChanged) {
    // calculate top positions of the group labels and lines
    var eventMargin = options.eventMargin,
      top = axisOnTop ?
        options.eventMarginAxis + eventMargin/2 :
        contentHeight - options.eventMarginAxis + eventMargin/2;

    for (var i = 0, iMax = groups.length; i < iMax; i++) {
      var group = groups[i];
      if (axisOnTop) {
        group.top = top;
        group.labelTop = top + axisHeight + (group.height - group.labelHeight) / 2;
        group.lineTop = top + axisHeight + group.height + eventMargin/2;
        top += group.height + eventMargin;
      }
      else {
        top -= group.height + eventMargin;
        group.top = top;
        group.labelTop = top + (group.height - group.labelHeight) / 2;
        group.lineTop = top - eventMargin/2;
      }
    }

    // calculate top position of the items
    for (var i = 0, iMax = items.length; i < iMax; i++) {
      var item = items[i],
        group = item.group;

      if (group) {
        item.top = group.top;
      }
    }

    resized = true;
  }

  resized = resized || (size.groupsWidth !== groupsWidth);
  resized = resized || (size.groupsLeft !== groupsLeft);
  resized = resized || (size.actualHeight !== actualHeight);
  size.groupsWidth = groupsWidth;
  size.groupsLeft = groupsLeft;
  size.actualHeight = actualHeight;

  resized = resized || (size.frameWidth !== frameWidth);
  resized = resized || (size.frameHeight !== frameHeight);
  size.frameWidth = frameWidth;
  size.frameHeight = frameHeight;

  resized = resized || (size.groupsWidth !== groupsWidth);
  size.groupsWidth = groupsWidth;
  size.contentLeft = options.groupsOnRight ? 0 : groupsWidth;
  size.contentWidth = Math.max(frameWidth - groupsWidth, 0);
  size.contentHeight = contentHeight;

  resized = resized || (size.axis.top !== axisTop);
  resized = resized || (size.axis.line !== axisLine);
  resized = resized || (size.axis.height !== axisHeight);
  resized = resized || (size.items.top !== itemsTop);
  size.axis.top = axisTop;
  size.axis.line = axisLine;
  size.axis.height = axisHeight;
  size.axis.labelMajorTop = options.axisOnTop ? 0 : axisLine + characterMinorHeight;
  size.axis.labelMinorTop = options.axisOnTop ?
    (options.showMajorLabels ? characterMajorHeight : 0) :
    axisLine;
  size.axis.lineMinorTop = options.axisOnTop ? size.axis.labelMinorTop : 0;
  size.axis.lineMinorHeight = options.showMajorLabels ?
    frameHeight - characterMajorHeight:
    frameHeight;
  size.axis.lineMinorWidth = dom.axis.minorLines.length ?
    dom.axis.minorLines[0].offsetWidth : 1;
  size.axis.lineMajorWidth = dom.axis.majorLines.length ?
    dom.axis.majorLines[0].offsetWidth : 1;

  size.items.top = itemsTop;

  resized = resized || (size.axis.characterMinorWidth  !== characterMinorWidth);
  resized = resized || (size.axis.characterMinorHeight !== characterMinorHeight);
  resized = resized || (size.axis.characterMajorWidth  !== characterMajorWidth);
  resized = resized || (size.axis.characterMajorHeight !== characterMajorHeight);
  size.axis.characterMinorWidth  = characterMinorWidth;
  size.axis.characterMinorHeight = characterMinorHeight;
  size.axis.characterMajorWidth  = characterMajorWidth;
  size.axis.characterMajorHeight = characterMajorHeight;

  // conversion factors can be changed when width of the Timeline is changed,
  // and when start or end are changed
  this.recalcConversion();

  return resized;
}



/**
 * Calculate the factor and offset to convert a position on screen to the
 * corresponding date and vice versa.
 * After the method calcConversionFactor is executed once, the methods screenToTime and
 * timeToScreen can be used.
 */
links.Timeline.prototype.recalcConversion = function() {
  this.conversion.offset = parseFloat(this.start.valueOf());
  this.conversion.factor = parseFloat(this.size.contentWidth) /
    parseFloat(this.end.valueOf() - this.start.valueOf());
}


/**
 * Convert a position on screen (pixels) to a datetime
 * Before this method can be used, the method calcConversionFactor must be
 * executed once.
 * @param {int}     x    Position on the screen in pixels
 * @return {Date}   time The datetime the corresponds with given position x
 */
links.Timeline.prototype.screenToTime = function(x) {
  var conversion = this.conversion,
    time = new Date(parseFloat(x) / conversion.factor + conversion.offset);
  return time;
}

/**
 * Convert a datetime (Date object) into a position on the screen
 * Before this method can be used, the method calcConversionFactor must be
 * executed once.
 * @param {Date}   time A date
 * @return {int}   x    The position on the screen in pixels which corresponds
 *                      with the given date.
 */
links.Timeline.prototype.timeToScreen = function(time) {
  var conversion = this.conversion;
  var x = (time.valueOf() - conversion.offset) * conversion.factor;
  return x;
}



/**
 * Event handler for touchstart event on mobile devices
 */
links.Timeline.prototype.onTouchStart = function(event) {
  var params = this.eventParams,
    dom = this.dom,
    me = this;

  if (params.touchDown) {
    // if already moving, return
    return;
  }

  params.touchDown = true;
  params.zoomed = false;

  this.onMouseDown(event);

  if (!params.onTouchMove) {
    params.onTouchMove = function (event) {me.onTouchMove(event);};
    links.Timeline.addEventListener(document, "touchmove", params.onTouchMove);
  }
  if (!params.onTouchEnd) {
    params.onTouchEnd  = function (event) {me.onTouchEnd(event);};
    links.Timeline.addEventListener(document, "touchend",  params.onTouchEnd);
  }
};

/**
 * Event handler for touchmove event on mobile devices
 */
links.Timeline.prototype.onTouchMove = function(event) {
  var params = this.eventParams;

  if (event.scale && event.scale !== 1) {
    params.zoomed = true;
  }

  if (!params.zoomed) {
    // move
    this.onMouseMove(event);
  }
  else {
    if (this.options.zoomable) {
      // pinch
      // TODO: pinch only supported on iPhone/iPad. Create something manually for Android?
      params.zoomed = true;

      var scale = event.scale,
        oldWidth = (params.end.valueOf() - params.start.valueOf()),
        newWidth = oldWidth / scale,
        diff = newWidth - oldWidth,
        start = new Date(parseInt(params.start.valueOf() - diff/2)),
        end = new Date(parseInt(params.end.valueOf() + diff/2));

      // TODO: determine zoom-around-date from touch positions?

      this.setVisibleChartRange(start, end);
      timeline.trigger("rangechange");

      links.Timeline.preventDefault(event);
    }
  }
};

/**
 * Event handler for touchend event on mobile devices
 */
links.Timeline.prototype.onTouchEnd = function(event) {
  var params = this.eventParams;
  params.touchDown = false;

  /* TODO: cleanup
  document.getElementById("info").innerHTML = "touchEnd";
  */

  if (params.zoomed) {
    timeline.trigger("rangechanged");
  }

  if (params.onTouchMove) {
    links.Timeline.removeEventListener(document, "touchmove", params.onTouchMove);
    delete params.onTouchMove;

  }
  if (params.onTouchEnd) {
    links.Timeline.removeEventListener(document, "touchend",  params.onTouchEnd);
    delete params.onTouchEnd;
  }

  this.onMouseUp(event);
};


/**
 * Start a moving operation inside the provided parent element
 * @param {event} event       The event that occurred (required for
 *                             retrieving the  mouse position)
 */
links.Timeline.prototype.onMouseDown = function(event) {
  event = event || window.event;

  var params = this.eventParams,
    options = this.options,
    dom = this.dom;

  // only react on left mouse button down
  var leftButtonDown = event.which ? (event.which == 1) : (event.button == 1);
  if (!leftButtonDown && !params.touchDown) {
    return;
  }

  // check if frame is not resized (causing a mismatch with the end Date)
  this.recalcSize();

  // get mouse position
  if (!params.touchDown) {
    params.mouseX = event.clientX;
    params.mouseY = event.clientY;
  }
  else {
    params.mouseX = event.targetTouches[0].clientX;
    params.mouseY = event.targetTouches[0].clientY;
  }
  if (params.mouseX === undefined) {params.mouseX = 0;}
  if (params.mouseY === undefined) {params.mouseY = 0;}
  params.frameLeft = links.Timeline.getAbsoluteLeft(this.dom.content);
  params.frameTop = links.Timeline.getAbsoluteTop(this.dom.content);
  params.previousLeft = 0;
  params.previousOffset = 0;

  params.moved = false;
  params.start = new Date(this.start);
  params.end = new Date(this.end);

  params.target = links.Timeline.getTarget(event);
  params.itemDragLeft = (params.target === this.dom.items.dragLeft);
  params.itemDragRight = (params.target === this.dom.items.dragRight);

  if (params.itemDragLeft || params.itemDragRight) {
    params.itemIndex = this.selection ? this.selection.index : undefined;
  }
  else {
    params.itemIndex = this.getItemIndex(params.target);
  }

  params.customTime = (params.target === dom.customTime ||
    params.target.parentNode === dom.customTime) ?
    this.customTime :
    undefined;

  params.addItem = (options.editable && event.ctrlKey);
  if (params.addItem) {
    // create a new event at the current mouse position
    var x = params.mouseX - params.frameLeft;
    var y = params.mouseY - params.frameTop;

    var xstart = this.screenToTime(x);
    if (options.snapEvents) {
      this.step.snap(xstart);
    }
    var xend = new Date(xstart);
    var content = "New";
    var group = this.getGroupFromHeight(y);
    this.addItem({
      'start': xstart,
      'end': xend,
      'content': content,
      'group': group.content
    });
    params.itemIndex = (this.items.length - 1);
    this.selectItem(params.itemIndex);
    params.itemDragRight = true;
  }

  params.editItem = options.editable ? this.isSelected(params.itemIndex) : undefined;
  if (params.editItem) {
    var item = this.items[params.itemIndex];
    params.itemStart = item.start;
    params.itemEnd = item.end;
    params.itemType = item.type;
    if (params.itemType == 'range') {
      params.itemLeft = this.timeToScreen(item.start);
      params.itemRight = this.timeToScreen(item.end);
    }
    else {
      params.itemLeft = this.timeToScreen(item.start);
    }
  }
  else {
    this.dom.frame.style.cursor = 'move';
  }
  if (!params.touchDown) {
    // add event listeners to handle moving the contents
    // we store the function onmousemove and onmouseup in the timeline, so we can
    // remove the eventlisteners lateron in the function mouseUp()
    var me = this;
    if (!params.onMouseMove) {
      params.onMouseMove = function (event) {me.onMouseMove(event);};
      links.Timeline.addEventListener(document, "mousemove", params.onMouseMove);
    }
    if (!params.onMouseUp) {
      params.onMouseUp = function (event) {me.onMouseUp(event);};
      links.Timeline.addEventListener(document, "mouseup", params.onMouseUp);
    }

    links.Timeline.preventDefault(event);
  }
}


/**
 * Perform moving operating.
 * This function activated from within the funcion links.Timeline.onMouseDown().
 * @param {event}   event  Well, eehh, the event
 */
links.Timeline.prototype.onMouseMove = function (event) {
  event = event || window.event;

  var params = this.eventParams,
    size = this.size,
    dom = this.dom,
    options = this.options;

  // calculate change in mouse position
  if (!params.touchDown) {
    var mouseX = event.clientX;
    var mouseY = event.clientY;
  }
  else {
    var mouseX = event.targetTouches[0].clientX;
    var mouseY = event.targetTouches[0].clientY;
  }
  if (mouseX === undefined) {mouseX = 0;}
  if (mouseY === undefined) {mouseY = 0;}

  if (params.mouseX === undefined) {
    params.mouseX = mouseX;
  }
  if (params.mouseY === undefined) {
    params.mouseY = mouseY;
  }

  var diffX = parseFloat(mouseX) - params.mouseX;
  var diffY = parseFloat(mouseY) - params.mouseY;

  params.moved = true;

  if (params.customTime) {
    var x = this.timeToScreen(params.customTime);
    var xnew = x + diffX;
    this.customTime = this.screenToTime(xnew);
    this.redrawCustomTime();

    // fire a timechange event
    this.trigger('timechange');
  }
  else if (params.editItem) {
    var item = this.items[params.itemIndex],
      domItem = item.dom,
      left,
      right;

    if (params.itemDragLeft) {
      // move the start of the item
      left = params.itemLeft + diffX;
      right = params.itemRight;

      item.start = this.screenToTime(left);
      if (options.snapEvents) {
        this.step.snap(item.start);
        left = this.timeToScreen(item.start);
      }

      if (left > right) {
        left = right;
        item.start = this.screenToTime(left);
      }
    }
    else if (params.itemDragRight) {
      // move the end of the item
      left = params.itemLeft;
      right = params.itemRight + diffX;

      item.end = this.screenToTime(right);
      if (options.snapEvents) {
        this.step.snap(item.end);
        right = this.timeToScreen(item.end);
      }

      if (right < left) {
        right = left;
        item.end = this.screenToTime(right);
      }
    }
    else {
      // move the item
      left = params.itemLeft + diffX;
      item.start = this.screenToTime(left);
      if (options.snapEvents) {
        this.step.snap(item.start);
        left = this.timeToScreen(item.start);
      }

      if (item.end) {
        right = left + (params.itemRight - params.itemLeft);
        item.end = this.screenToTime(right);
      }
    }

    switch(item.type) {
      case 'range':
        domItem.style.left = left + "px";
        //domItem.style.width = Math.max(right - left - 2 * item.borderWidth, 1) + "px";  // TODO
        domItem.style.width = Math.max(right - left, 1) + "px";
        break;

      case 'box':
        domItem.style.left = (left - item.width / 2) + "px";
        domItem.line.style.left = (left - item.lineWidth / 2) + "px";
        domItem.dot.style.left = (left - item.dotWidth / 2) + "px";
        break;

      case 'dot':
        domItem.style.left = (left - item.dotWidth / 2) + "px";
        break;
    }

    if (this.groups.length == 0) {
      // TODO: does not work well in FF, forces redraw with every mouse move it seems
      this.stackEvents(options.animate);
      if (!options.animate) {
        this.redrawFrame();
      }
      // Note: when animate==true, no redraw is needed here, its done by stackEvents animation
    }
    else {
      /* TODO: move item from one group to another when needed
      var y = mouseY - params.frameTop;
      var group = this.getGroupFromHeight(y);
      if (item.group !== group) {
        // ... move item to the other group
      }
      */
    }

    this.redrawDeleteButton();
    this.redrawDragAreas();
  }
  else if (options.moveable) {
    var interval = (params.end.valueOf() - params.start.valueOf());
    var diffMillisecs = parseFloat(-diffX) / size.contentWidth * interval;
    this.start = new Date(params.start.valueOf() + Math.round(diffMillisecs));
    this.end = new Date(params.end.valueOf() + Math.round(diffMillisecs));

    this.recalcConversion();

    // move the items by changing the left position of their frame.
    // this is much faster than repositioning all elements individually via the
    // redrawFrame() function (which is done once at mouseup)
    // note that we round diffX to prevent wrong positioning on millisecond scale
    var previousLeft = params.previousLeft || 0;
    var currentLeft = parseFloat(dom.items.frame.style.left) || 0;
    var previousOffset = params.previousOffset || 0;
    var frameOffset = previousOffset + (currentLeft - previousLeft);
    var frameLeft = -Math.round(diffMillisecs) / interval * size.contentWidth + frameOffset;
    params.previousOffset = frameOffset;
    params.previousLeft = frameLeft;

    dom.items.frame.style.left = (frameLeft) + "px";

    this.redrawCurrentTime();
    this.redrawCustomTime();
    this.redrawAxis();

    // fire a rangechange event
    this.trigger('rangechange');
  }

  links.Timeline.preventDefault(event);
}


/**
 * Stop moving operating.
 * This function activated from within the funcion links.Timeline.onMouseDown().
 * @param {event}  event   The event
 */
links.Timeline.prototype.onMouseUp = function (event) {
  var params = this.eventParams,
    options = this.options;

  event = event || window.event;

  this.dom.frame.style.cursor = 'auto';

  // remove event listeners here, important for Safari
  if (params.onMouseMove) {
    links.Timeline.removeEventListener(document, "mousemove", params.onMouseMove);
    delete params.onMouseMove;
  }
  if (params.onMouseUp) {
    links.Timeline.removeEventListener(document, "mouseup",   params.onMouseUp);
    delete params.onMouseUp;
  }
  //links.Timeline.preventDefault(event);

  if (params.customTime) {
    // fire a timechanged event
    this.trigger('timechanged');
  }
  else if (params.editItem) {
    var item = this.items[params.itemIndex];

    if (params.moved || params.addItem) {
      this.applyChange = true;
      this.applyAdd = true;

      this.updateData(params.itemIndex, {
        'start': item.start,
        'end': item.end
      });

      // fire an add or change event.
      // Note that the change can be canceled from within an event listener if
      // this listener calls the method cancelChange().
      this.trigger(params.addItem ? 'add' : 'change');

      if (params.addItem) {
        if (this.applyAdd) {
          this.updateData(params.itemIndex, {
            'start': item.start,
            'end': item.end,
            'content': item.content,
            'group': item.group ? item.group.content : undefined
          });
        }
        else {
          // undo an add
          this.deleteItem(params.itemIndex);
        }
      }
      else {
        if (this.applyChange) {
          this.updateData(params.itemIndex, {
            'start': item.start,
            'end': item.end
          });
        }
        else {
          // undo a change
          delete this.applyChange;
          delete this.applyAdd;

          var item = this.items[params.itemIndex],
            domItem = item.dom;

          item.start = params.itemStart;
          item.end = params.itemEnd;
          domItem.style.left = params.itemLeft + "px";
          domItem.style.width = (params.itemRight - params.itemLeft) + "px";
        }
      }

      this.recalcSize();
      this.stackEvents(options.animate);
      if (!options.animate) {
        this.redrawFrame();
      }
      this.redrawDeleteButton();
      this.redrawDragAreas();
    }
  }
  else {
    if (!params.moved && !params.zoomed) {
      // mouse did not move -> user has selected an item

      if (options.editable && (params.target === this.dom.items.deleteButton)) {
        // delete item
        if (this.selection) {
          this.confirmDeleteItem(this.selection.index);
        }
        this.redrawFrame();
      }
      else if (options.selectable) {
        // select/unselect item
        if (params.itemIndex !== undefined) {
          if (!this.isSelected(params.itemIndex)) {
            this.selectItem(params.itemIndex);
            this.trigger('select');
          }
        }
        else {
          this.unselectItem();
        }
        this.redrawDeleteButton();
      }
    }
    else {
      // timeline is moved
      this.redrawFrame();

      if ((params.moved && options.moveable) || (params.zoomed && options.zoomable) ) {
        // fire a rangechanged event
        this.trigger('rangechanged');
      }
    }
  }
}

/**
 * Double click event occurred for an item
 * @param {event}  event
 */
links.Timeline.prototype.onDblClick = function (event) {
  var params = this.eventParams,
    options = this.options,
    dom = this.dom,
    size = this.size;
  event = event || window.event;

  if (!options.editable) {
    return;
  }

  if (params.itemIndex !== undefined) {
    // fire the edit event
    this.trigger('edit');
  }
  else {
    // create a new item
    var x = event.clientX - links.Timeline.getAbsoluteLeft(dom.content);
    var y = event.clientY - links.Timeline.getAbsoluteTop(dom.content);

    // create a new event at the current mouse position
    var xstart = this.screenToTime(x);
    var xend = this.screenToTime(x  + size.frameWidth / 10); // add 10% of timeline width
    if (options.snapEvents) {
      this.step.snap(xstart);
      this.step.snap(xend);
    }

    var content = "New";
    var group = this.getGroupFromHeight(y);   // (group may be undefined)
    this.addItem({
      'start': xstart,
      'end': xend,
      'content': content,
      'group': group.content
    });
    params.itemIndex = (this.items.length - 1);
    this.selectItem(params.itemIndex);

    this.applyAdd = true;

    // fire an add event.
    // Note that the change can be canceled from within an event listener if
    // this listener calls the method cancelAdd().
    this.trigger('add');

    if (!this.applyAdd) {
      // undo an add
      this.deleteItem(params.itemIndex);
    }

    this.redrawDeleteButton();
    this.redrawDragAreas();
  }

  links.Timeline.preventDefault(event);
}


/**
 * Event handler for mouse wheel event, used to zoom the timeline
 * Code from http://adomas.org/javascript-mouse-wheel/
 * @param {event}  event   The event
 */
links.Timeline.prototype.onMouseWheel = function(event) {
  if (!this.options.zoomable)
    return;

  if (!event) { /* For IE. */
    event = window.event;
  }

  // retrieve delta
  var delta = 0;
  if (event.wheelDelta) { /* IE/Opera. */
    delta = event.wheelDelta/120;
  } else if (event.detail) { /* Mozilla case. */
    // In Mozilla, sign of delta is different than in IE.
    // Also, delta is multiple of 3.
    delta = -event.detail/3;
  }

  // If delta is nonzero, handle it.
  // Basically, delta is now positive if wheel was scrolled up,
  // and negative, if wheel was scrolled down.
  if (delta) {
    // TODO: on FireFox, the window is not redrawn within repeated scroll-events
    // -> use a delayed redraw? Make a zoom queue?

    var timeline = this;
    var zoom = function () {
      // check if frame is not resized (causing a mismatch with the end date)
      timeline.recalcSize();

      // perform the zoom action. Delta is normally 1 or -1
      var zoomFactor = delta / 5.0;
      var frameLeft = links.Timeline.getAbsoluteLeft(timeline.dom.content);
      var zoomAroundDate =
        (event.clientX != undefined && frameLeft != undefined) ?
        timeline.screenToTime(event.clientX - frameLeft) :
        undefined;

      timeline.zoom(zoomFactor, zoomAroundDate);

      // fire a rangechange and a rangechanged event
      timeline.trigger("rangechange");
      timeline.trigger("rangechanged");

      /* TODO: smooth scrolling on FF
      timeline.zooming = false;

      if (timeline.zoomingQueue) {
        setTimeout(timeline.zoomingQueue, 100);
        timeline.zoomingQueue = undefined;
      }

      timeline.zoomCount = (timeline.zoomCount || 0) + 1;
      console.log('zoomCount', timeline.zoomCount)
      */
    };

    zoom();

    /* TODO: smooth scrolling on FF
    if (!timeline.zooming || true) {

      timeline.zooming = true;
      setTimeout(zoom, 100);
    }
    else {
      timeline.zoomingQueue = zoom;
    }
    //*/
  }

  // Prevent default actions caused by mouse wheel.
  // That might be ugly, but we handle scrolls somehow
  // anyway, so don't bother here...
  links.Timeline.preventDefault(event);
}


/**
 * Zoom the timeline the given zoomfactor in or out. Start and end date will
 * be adjusted, and the timeline will be redrawn. You can optionally give a
 * date around which to zoom.
 * For example, try zoomfactor = 0.1 or -0.1
 * @param {float}  zoomFactor      Zooming amount. Positive value will zoom in,
 *                                 negative value will zoom out
 * @param {Date}   zoomAroundDate  Date around which will be zoomed. Optional
 */
links.Timeline.prototype.zoom = function(zoomFactor, zoomAroundDate) {
  // if zoomAroundDate is not provided, take it half between start Date and end Date
  if (zoomAroundDate == undefined) {
    zoomAroundDate = new Date((this.start.valueOf() + this.end.valueOf()) / 2);
  }

  // prevent zoom factor larger than 1 or smaller than -1 (larger than 1 will
  // result in a start>=end )
  if (zoomFactor >= 1) {
    zoomFactor = 0.9;
  }
  if (zoomFactor <= -1) {
    zoomFactor = -0.9;
  }

  // adjust a negative factor such that zooming in with 0.1 equals zooming
  // out with a factor -0.1
  if (zoomFactor < 0) {
    zoomFactor = zoomFactor / (1 + zoomFactor);
  }

  // zoom start Date and end Date relative to the zoomAroundDate
  var startDiff = parseFloat(this.start.valueOf() - zoomAroundDate.valueOf());
  var endDiff = parseFloat(this.end.valueOf() - zoomAroundDate.valueOf());

  // calculate new dates
  var newStart = new Date(this.start.valueOf() - startDiff * zoomFactor);
  var newEnd   = new Date(this.end.valueOf() - endDiff * zoomFactor);

  // prevent scale of less than 10 milliseconds
  // TODO: IE has problems with milliseconds
  if (zoomFactor > 0 && (newEnd.valueOf() - newStart.valueOf()) < 10) {
    return;
  }

  // prevent scale of more than than 10 thousand years
  if (zoomFactor < 0 && (newEnd.getFullYear() - newStart.getFullYear()) > 10000) {
    return;
  }

  // apply new dates
  this.start = newStart;
  this.end = newEnd;

  this.recalcSize();
  var animate = this.options.animate ? this.options.animateZoom : false;
  this.stackEvents(animate);
  if (!animate || this.groups.length > 0) {
    this.redrawFrame();
  }
  /* TODO
  else {
    this.redrawFrame();
    this.recalcSize();
    this.stackEvents(animate);
    this.redrawFrame();
  }*/
}


/**
 * Move the timeline the given movefactor to the left or right. Start and end
 * date will be adjusted, and the timeline will be redrawn.
 * For example, try moveFactor = 0.1 or -0.1
 * @param {float}  moveFactor      Moving amount. Positive value will move right,
 *                                 negative value will move left
 */
links.Timeline.prototype.move = function(moveFactor) {
  // zoom start Date and end Date relative to the zoomAroundDate
  var diff = parseFloat(this.end.valueOf() - this.start.valueOf());

  // apply new dates
  this.start = new Date(this.start.valueOf() + diff * moveFactor);
  this.end   = new Date(this.end.valueOf() + diff * moveFactor);

  this.recalcConversion();
  this.redrawFrame();
}

/**
 * Delete an item after a confirmation.
 * The deletion can be cancelled by executing .cancelDelete() during the
 * triggered event 'delete'.
 * @param {int} index   Index of the item to be deleted
 */
links.Timeline.prototype.confirmDeleteItem = function(index) {
  this.applyDelete = true;

  // select the event to be deleted
  if (!this.isSelected(index)) {
    this.selectItem(index);
  }

  // fire a delete event trigger.
  // Note that the delete event can be canceled from within an event listener if
  // this listener calls the method cancelChange().
  this.trigger('delete');

  if (this.applyDelete) {
    this.deleteItem(index);
  }

  delete this.applyDelete;
}

/**
 * Delete an item
 * @param {int} index   Index of the item to be deleted
 */
links.Timeline.prototype.deleteItem = function(index) {
  if (index >= this.items.length) {
    throw "Cannot delete row, index out of range";
  }

  this.unselectItem();

  // actually delete the item
  this.items.splice(index, 1);

  // delete the row in the original data table
  if (this.data) {
    if (google && google.visualization &&
        this.data instanceof google.visualization.DataTable) {
      this.data.removeRow(index);
    }
    else if (links.Timeline.isArray(this.data)) {
      this.data.splice(index, 1);
    }
    else {
      throw "Cannot delete row from data, unknown data type";
    }
  }

  this.size.dataChanged = true;
  this.redrawFrame();
  this.recalcSize();
  this.stackEvents(this.options.animate);
  if (!this.options.animate) {
    this.redrawFrame();
  }
  this.size.dataChanged = false;
}


/**
 * Delete all items
 */
links.Timeline.prototype.deleteAllItems = function() {
  this.unselectItem();

  // delete the loaded data
  this.items = [];

  // delete the groups
  this.deleteGroups();

  // empty original data table
  if (this.data) {
    if (google && google.visualization &&
        this.data instanceof google.visualization.DataTable) {
      this.data.removeRows(0, this.data.getNumberOfRows());
    }
    else if (links.Timeline.isArray(this.data)) {
      this.data.splice(0, this.data.length);
    }
    else {
      throw "Cannot delete row from data, unknown data type";
    }
  }

  this.size.dataChanged = true;
  this.redrawFrame();
  this.recalcSize();
  this.stackEvents(this.options.animate);
  if (!this.options.animate) {
    this.redrawFrame();
  }
  this.size.dataChanged = false;

}


/**
 * Find the group from a given height in the timeline
 * @param {Number} height   Height in the timeline
 * @return {Object} group   The group object, or undefined if out of range
 */
links.Timeline.prototype.getGroupFromHeight = function(height) {
  var groups = this.groups,
    options = this.options,
    size = this.size,
    y = height - (options.axisOnTop ? size.axis.height : 0);

  if (groups) {
    var group;
    for (var i = 0, iMax = groups.length; i < iMax; i++) {
      group = groups[i];
      if (y > group.top && y < group.top + group.height) {
        return group;
      }
    }

    return group; // return the last group
  }

  return undefined;
}

/**
 * Retrieve the properties of an item.
 * @param {Number} index
 * @return {Object} properties   Object containing item properties:<br>
 *                              {Date} start (required),
 *                              {Date} end (optional),
 *                              {String} content (required),
 *                              {String} group (optional)
 */
links.Timeline.prototype.getItem = function (index) {
  if (index >= this.items.length) {
    throw "Cannot get item, index out of range";
  }

  var item = this.items[index];

  var properties = {};
  properties.start = new Date(item.start);
  if (item.end) {
    properties.end = new Date(item.end);
  }
  properties.content = item.content;
  if (item.group) {
    properties.group = item.group.content;
  }

  return properties;
}

/**
 * Add a new item.
 * @param {Object} itemData     Object containing item properties:<br>
 *                              {Date} start (required),
 *                              {Date} end (optional),
 *                              {String} content (required),
 *                              {String} group (optional)
 */
links.Timeline.prototype.addItem = function (itemData) {
  var items = [
    itemData
  ];

  this.addItems(items);
}

/**
 * Add new items.
 * @param {Array} items  An array containing Objects.
 *                       The objects must have the following parameters:
 *                         {Date} start,
 *                         {Date} end,
 *                         {String} content with text or HTML code,
 *                         {String} group
 */
links.Timeline.prototype.addItems = function (items) {
  var newItems = items,
    curItems = this.items,
    groups = this.groups,
    groupIndexes = this.groupIndexes;
  // append the items
  for (var i = 0, iMax = newItems.length; i < iMax; i++) {
    var itemData = items[i];
    this.addGroup(itemData.group);

    curItems.push(this.createItem(itemData));
    var index = curItems.length - 1;
    this.updateData(index, itemData);
  }

  // redraw timeline
  this.size.dataChanged = true;
  this.redrawFrame();
  this.recalcSize();
  this.stackEvents(false);
  this.redrawFrame();
  this.size.dataChanged = false;
}

/**
 * Create an item object, containing all needed parameters
 * @param {Object} itemData  Object containing parameters start, end
 *                           content, group.
 * @return {Object} item
 */
links.Timeline.prototype.createItem = function(itemData) {
  var item = {
    'start': itemData.start,
    'end': itemData.end,
    'content': itemData.content,
    'type': itemData.end ? 'range' : this.options.style,
    'group': this.findGroup(itemData.group),
    'top': 0,
    'left': 0,
    'width': 0,
    'height': 0,
    'lineWidth' : 0,
    'dotWidth': 0,
    'dotHeight': 0
  };
  return item;
}

/**
 * Edit an item
 * @param {Number} index
 * @param {Object} itemData     Object containing item properties:<br>
 *                              {Date} start (required),
 *                              {Date} end (optional),
 *                              {String} content (required),
 *                              {String} group (optional)
 */
links.Timeline.prototype.changeItem = function (index, itemData) {
  if (index >= this.items.length) {
    throw "Cannot change item, index out of range";
  }

  var style = this.options.style;
  var item = this.items[index];

  // edit the item
  if (itemData.start) {
    item.start = itemData.start;
  }
  if (itemData.end) {
    item.end = itemData.end;
  }
  if (itemData.content) {
    item.content = itemData.content;
  }
  if (itemData.group) {
    item.group = this.addGroup(itemData.group);
  }

  // update the original data table
  this.updateData(index, itemData);

  // redraw timeline
  this.size.dataChanged = true;
  this.redrawFrame();
  this.recalcSize();
  this.stackEvents(false);
  this.redrawFrame();
  this.size.dataChanged = false;
}


/**
 * Find a group by its name.
 * @param {String} group
 * @return {Object} a group object or undefined when group is not found
 */
links.Timeline.prototype.findGroup = function (group) {
  var index = this.groupIndexes[group];
  return (index != undefined) ? this.groups[index] : undefined;
}

/**
 * Delete all groups
 */
links.Timeline.prototype.deleteGroups = function () {
  this.groups = [];
  this.groupIndexes = {};
}


/**
 * Add a group. When the group already exists, no new group is created
 * but the existing group is returned.
 * @param {String} groupName   the name of the group
 * @return {Object} groupObject
 */
links.Timeline.prototype.addGroup = function (groupName) {
  var groups = this.groups,
    groupIndexes = this.groupIndexes;

  var groupObj = groupIndexes[groupName];
  if (groupObj === undefined && groupName !== undefined) {
    var groupObj = {
      'content': groupName,
      'labelTop': 0,
      'lineTop': 0
      // note: this object will lateron get addition information,
      //       such as height and width of the group
    };
    groups.push(groupObj);

    // sort the groups
    if (this.options.axisOnTop) {
      groups.sort(function (a, b) {
        return a.content > b.content;
      });
    }
    else {
      groups.sort(function (a, b) {
        return a.content < b.content;
      });
    }

    // rebuilt the groupIndexes
    for (var i = 0, iMax = groups.length; i < iMax; i++) {
      groupIndexes[groups[i].content] = i;
    }
  }

  return groupObj;
}

/**
 * Cancel a change item
 * This method can be called insed an event listener which catches the "change"
 * event. The changed event position will be undone.
 */
links.Timeline.prototype.cancelChange = function () {
  this.applyChange = false;
}

/**
 * Cancel deletion of an item
 * This method can be called insed an event listener which catches the "delete"
 * event. Deletion of the event will be undone.
 */
links.Timeline.prototype.cancelDelete = function () {
  this.applyDelete = false;
}


/**
 * Cancel creation of a new item
 * This method can be called insed an event listener which catches the "new"
 * event. Creation of the new the event will be undone.
 */
links.Timeline.prototype.cancelAdd = function () {
  this.applyAdd = false;
}


/**
 * Select an event. The visible chart range will be moved such that the selected
 * event is placed in the middle.
 * For example selection = [{row: 5}];
 * @param {array} sel   An array with a column row, containing the row number
 *                      (the id) of the event to be selected.
 * @return {boolean}    true if selection is succesfully set, else false.
 */
links.Timeline.prototype.setSelection = function(selection) {
  if (selection != undefined && selection.length > 0) {
    if (selection[0].row != undefined) {
      var index = selection[0].row;
      if (this.items[index]) {
        var item = this.items[index];
        this.selectItem(index);

        // move the visible chart range to the selected event.
        var start = item.start;
        var end = item.end;
        if (end != undefined) {
          var middle = new Date((end.valueOf() + start.valueOf()) / 2);
        } else {
          var middle = new Date(start);
        }
        var diff = (this.end.valueOf() - this.start.valueOf()),
          newStart = new Date(middle.valueOf() - diff/2),
          newEnd = new Date(middle.valueOf() + diff/2);

        this.setVisibleChartRange(newStart, newEnd);

        return true;
      }
    }
  }
  return false;
}

/**
 * Retrieve the currently selected event
 * @return {array} sel  An array with a column row, containing the row number
 *                      of the selected event. If there is no selection, an
 *                      empty array is returned.
 */
links.Timeline.prototype.getSelection = function() {
  var sel = [];
  if (this.selection) {
    sel.push({"row": this.selection.index});
  }
  return sel;
}


/**
 * Select an item by its index
 * @param {Number} index
 */
links.Timeline.prototype.selectItem = function(index) {
  this.unselectItem();

  this.selection = undefined;

  if (this.items[index] !== undefined) {
    var item = this.items[index],
      domItem = item.dom;

    this.selection = {
      'index': index,
      'item': domItem
    };

    if (this.options.editable) {
      domItem.style.cursor = 'move';
    }
    switch (item.type) {
      case 'range':
        domItem.className = "timeline-event timeline-event-selected timeline-event-range";
        break;
      case 'box':
        domItem.className = "timeline-event timeline-event-selected timeline-event-box";
        domItem.line.className = "timeline-event timeline-event-selected timeline-event-line";
        domItem.dot.className = "timeline-event timeline-event-selected timeline-event-dot";
        break;
      case 'dot':
        domItem.className = "timeline-event timeline-event-selected";
        domItem.dot.className = "timeline-event timeline-event-selected timeline-event-dot";
        break;
    }
  }
}

/**
 * Check if an item is currently selected
 * @param {Number} index
 * @return {boolean} true if row is selected, else false
 */
links.Timeline.prototype.isSelected = function (index) {
  return (this.selection && this.selection.index === index);
}

/**
 * Unselect the currently selected event (if any)
 */
links.Timeline.prototype.unselectItem = function() {
  if (this.selection) {
    var item = this.items[this.selection.index];

    if (item && item.dom) {
      var domItem = item.dom;
      domItem.style.cursor = '';
      switch (item.type) {
        case 'range':
          domItem.className = "timeline-event timeline-event-range";
          break;
        case 'box':
          domItem.className = "timeline-event timeline-event-box";
          domItem.line.className = "timeline-event timeline-event-line";
          domItem.dot.className = "timeline-event timeline-event-dot";
          break;
        case 'dot':
          domItem.className = "";
          domItem.dot.className = "timeline-event timeline-event-dot";
          break;
      }
    }
  }

  this.selection = undefined;
}


/**
 * Stack the items such that they don't overlap. The items will have a minimal
 * distance equal to options.eventMargin.
 * @param {boolean} animate     if animate is true, the items are moved to
 *                              their new position animated
 */
links.Timeline.prototype.stackEvents = function(animate) {
  if (this.options.stackEvents == false || this.groups.length > 0) {
    // under this conditions we refuse to stack the events
    return;
  }

  if (animate == undefined) {
    animate = false;
  }

  var sortedItems = this.stackOrder(this.items);
  var finalItems = this.stackCalculateFinal(sortedItems, animate);

  if (animate) {
    // move animated to the final positions
    var animation = this.animation;
    if (!animation) {
      animation = {};
      this.animation = animation;
    }
    animation.finalItems = finalItems;

    var timeline = this;
    var step = function () {
      var arrived = timeline.stackMoveOneStep(sortedItems, animation.finalItems);

      timeline.recalcSize();
      timeline.redrawFrame();

      if (!arrived) {
        animation.timer = setTimeout(step, 30);
      }
      else {
        delete animation.finalItems;
        delete animation.timer;
      }
    }

    if (!animation.timer) {
      animation.timer = setTimeout(step, 30);
    }
  }
  else {
    this.stackMoveToFinal(sortedItems, finalItems);
    this.recalcSize();
    //this.redraw(); // TODO: cleanup
  }
}


/**
 * Order the items in the array this.items. The order is determined via:
 * - Ranges go before boxes and dots.
 * - The item with the left most location goes first
 * @param {Array} items        Array with items
 * @return {Array} sortedItems Array with sorted items
 */
links.Timeline.prototype.stackOrder = function(items) {
  // TODO: store the sorted items, to have less work later on
  var sortedItems = items.concat([]);

  var f = function (a, b) {
    if (a.type == 'range' && b.type != 'range') {
      return -1;
    }

    if (a.type != 'range' && b.type == 'range') {
      return 1;
    }

    return (a.left - b.left);
  };

  sortedItems.sort(f);

  return sortedItems;
}

/**
 * Adjust vertical positions of the events such that they don't overlap each
 * other.
 */
links.Timeline.prototype.stackCalculateFinal = function(items) {
  var size = this.size,
    axisTop = size.axis.top,
    options = this.options,
    axisOnTop = options.axisOnTop,
    eventMargin = options.eventMargin,
    eventMarginAxis = options.eventMarginAxis,
    finalItems = [];

  // initialize final positions
  for (var i = 0, iMax = items.length; i < iMax; i++) {
    var item = items[i],
      top,
      left,
      right,
      bottom,
      height = item.height,
      width = item.width;

    if (axisOnTop) {
      top = axisTop + eventMarginAxis + eventMargin / 2;
    }
    else {
      top = axisTop - height - eventMarginAxis - eventMargin / 2;
    }
    bottom = top + height;

    switch (item.type) {
      case 'range':
      case 'dot':
        left = this.timeToScreen(item.start);
        right = item.end ? this.timeToScreen(item.end) : left + width;
        break;

      case 'box':
        left = this.timeToScreen(item.start) - width / 2;
        right = left + width;
        break;
    }

    finalItems[i] = {
      'left': left,
      'top': top,
      'right': right,
      'bottom': bottom,
      'height': height,
      'item': item
    };
  }

  // calculate new, non-overlapping positions
  //var items = sortedItems;
  for (var i = 0, iMax = finalItems.length; i < iMax; i++) {
  //for (var i = finalItems.length - 1; i >= 0; i--) {
    var finalItem = finalItems[i];
    var collidingItem = null;
    do {
      // TODO: optimize checking for overlap. when there is a gap without items,
      //  you only need to check for items from the next item on, not from zero
      collidingItem = this.stackEventsCheckOverlap(finalItems, i, 0, i-1);
      if (collidingItem != null) {
        // There is a collision. Reposition the event above the colliding element
        if (axisOnTop) {
          finalItem.top = collidingItem.top + collidingItem.height + eventMargin;
        }
        else {
          finalItem.top = collidingItem.top - finalItem.height - eventMargin;
        }
        finalItem.bottom = finalItem.top + finalItem.height;
      }
    } while (collidingItem);
  }

  return finalItems;
}


/**
 * Move the events one step in the direction of their final positions
 * @param {Array} currentItems   Array with the real items and their current
 *                               positions
 * @param {Array} finalItems     Array with objects containing the final
 *                               positions of the items
 * @return {boolean} arrived     True if all items have reached their final
 *                               location, else false
 */
links.Timeline.prototype.stackMoveOneStep = function(currentItems, finalItems) {
  // TODO: check this method
  var arrived = true;

  // apply new positions animated
  for (i = 0, iMax = currentItems.length; i < iMax; i++) {
    var finalItem = finalItems[i],
      item = finalItem.item;

    var topNow = parseInt(item.top);
    var topFinal = parseInt(finalItem.top);
    var diff = (topFinal - topNow);
    if (diff) {
      var step = (topFinal == topNow) ? 0 : ((topFinal > topNow) ? 1 : -1);
      if (Math.abs(diff) > 4) step = diff / 4;
      var topNew = parseInt(topNow + step);

      if (topNew != topFinal) {
        arrived = false;
      }

      item.top = topNew;
      item.bottom = item.top + item.height;
    }
    else {
      item.top = finalItem.top;
      item.bottom = finalItem.bottom;
    }

    item.left = finalItem.left;
    item.right = finalItem.right;
  }

  return arrived;
}



/**
 * Move the events from their current position to the final position
 * @param {Array} currentItems   Array with the real items and their current
 *                               positions
 * @param {Array} finalItems     Array with objects containing the final
 *                               positions of the items
 */
links.Timeline.prototype.stackMoveToFinal = function(currentItems, finalItems) {
  // Put the events directly at there final position
  for (i = 0, iMax = currentItems.length; i < iMax; i++) {
    var current = currentItems[i],
      finalItem = finalItems[i];

    current.left = finalItem.left;
    current.top = finalItem.top;
    current.right = finalItem.right;
    current.bottom = finalItem.bottom;
  }
}



/**
 * Check if the destiny position of given item overlaps with any
 * of the other items from index itemStart to itemEnd.
 * @param {Array} items      Array with items
 * @param {int}  itemIndex   Number of the item to be checked for overlap
 * @param {int}  itemStart   First item to be checked.
 * @param {int}  itemEnd     Last item to be checked.
 * @return {Object}          colliding item, or undefined when no collisions
 */
links.Timeline.prototype.stackEventsCheckOverlap = function(items, itemIndex,
    itemStart, itemEnd) {
    eventMargin = this.options.eventMargin,
    collision = this.collision;

  /* TODO: cleanup
  var item1 = items[itemIndex];
  for (var i = itemStart; i <= itemEnd; i++) {
    var item2 = items[i];
    if (collision(item1, item2, eventMargin)) {
      if (i != itemIndex) {
        return item2;
      }
    }
  }
  return;
  //*/

  // we loop from end to start, as we suppose that the chance of a
  // collision is larger for items at the end, so check these first.
  var item1 = items[itemIndex];
  for (var i = itemEnd; i >= itemStart; i--) {
    var item2 = items[i];
    if (collision(item1, item2, eventMargin)) {
      if (i != itemIndex) {
        return item2;
      }
    }
  }
}

/**
 * Test if the two provided items collide
 * The items must have parameters left, right, top, and bottom.
 * @param {htmlelement} item1   The first item
 * @param {htmlelement} item2    The second item
 * @param {int}         margin  A minimum required margin. Optional.
 *                              If margin is provided, the two items will be
 *                              marked colliding when they overlap or
 *                              when the margin between the two is smaller than
 *                              the requested margin.
 * @return {boolean}            true if item1 and item2 collide, else false
 */
links.Timeline.prototype.collision = function(item1, item2, margin) {
  // set margin if not specified
  if (margin == undefined) {
    margin = 0;
  }

  // calculate if there is overlap (collision)
  return (item1.left - margin < item2.right &&
          item1.right + margin > item2.left &&
          item1.top - margin < item2.bottom &&
          item1.bottom + margin > item2.top);
}


/**
 * fire an event
 * @param {String} event   The name of an event, for example "rangechange" or "edit"
 */
links.Timeline.prototype.trigger = function (event) {
  // built up properties
  var properties = null;
  switch (event) {
    case 'rangechange':
    case 'rangechanged':
      properties = {
        'start': new Date(this.start),
        'end': new Date(this.end)
      };
      break;

    case 'timechange':
    case 'timechanged':
      properties = {
        'time': new Date(this.customTime)
      };
      break;
  }

  // trigger the links event bus
  links.events.trigger(this, event, properties);

  // trigger the google event bus
  if (google && google.visualization) {
    google.visualization.events.trigger(this, event, properties);
  }
}



/** ------------------------------------------------------------------------ **/


/**
 * Event listener (singleton)
 */
links.events = links.events || {
  'listeners': [],

  /**
   * Find a single listener by its object
   * @param {Object} object
   * @return {Number} index  -1 when not found
   */
  'indexOf': function (object) {
    var listeners = this.listeners;
    for (var i = 0, iMax = this.listeners.length; i < iMax; i++) {
      var listener = listeners[i];
      if (listener && listener.object == object) {
        return i;
      }
    }
    return -1;
  },

  /**
   * Add an event listener
   * @param {Object} object
   * @param {String} event       The name of an event, for example 'select'
   * @param {function} callback  The callback method, called when the
   *                             event takes place
   */
  'addListener': function (object, event, callback) {
    var index = this.indexOf(object);
    var listener = this.listeners[index];
    if (!listener) {
      listener = {
        'object': object,
        'events': {}
      };
      this.listeners.push(listener);
    }

    var callbacks = listener.events[event];
    if (!callbacks) {
      callbacks = [];
      listener.events[event] = callbacks;
    }

    // add the callback if it does not yet exist
    if (callbacks.indexOf(callback) == -1) {
      callbacks.push(callback);
    }
  },

  /**
   * Remove an event listener
   * @param {Object} object
   * @param {String} event       The name of an event, for example 'select'
   * @param {function} callback  The registered callback method
   */
  'removeListener': function (object, event, callback) {
    var index = this.indexOf(object);
    var listener = this.listeners[index];
    if (listener) {
      var callbacks = listener.events[event];
      if (callbacks) {
        var index = callbacks.indexOf(callback);
        if (index != -1) {
          callbacks.splice(index, 1);
        }

        // remove the array when empty
        if (callbacks.length == 0) {
          delete listener.events[event];
        }
      }

      // count the number of registered events. remove listener when empty
      var count = 0;
      var events = listener.events;
      for (var event in events) {
        if (events.hasOwnProperty(event)) {
          count++;
        }
      }
      if (count == 0) {
        delete this.listeners[index];
      }
    }
  },

  /**
   * Remove all registered event listeners
   */
  'removeAllListeners': function () {
    this.listeners = [];
  },

  /**
   * Trigger an event. All registered event handlers will be called
   * @param {Object} object
   * @param {String} event
   * @param {Object} properties (optional)
   */
  'trigger': function (object, event, properties) {
    var index = this.indexOf(object);
    var listener = this.listeners[index];
    if (listener) {
      var callbacks = listener.events[event];
      if (callbacks) {
        for (var i = 0, iMax = callbacks.length; i < iMax; i++) {
          callbacks[i](properties);
        }
      }
    }
  }
};


/** ------------------------------------------------------------------------ **/

/**
 * @class StepDate
 * The class StepDate is an iterator for dates. You provide a start date and an
 * end date. The class itself determines the best scale (step size) based on the
 * provided start Date, end Date, and minimumStep.
 *
 * If minimumStep is provided, the step size is chosen as close as possible
 * to the minimumStep but larger than minimumStep. If minimumStep is not
 * provided, the scale is set to 1 DAY.
 * The minimumStep should correspond with the onscreen size of about 6 characters
 *
 * Alternatively, you can set a scale by hand.
 * After creation, you can initialize the class by executing start(). Then you
 * can iterate from the start date to the end date via next(). You can check if
 * the end date is reached with the function end(). After each step, you can
 * retrieve the current date via get().
 * The class step has scales ranging from milliseconds, seconds, minutes, hours,
 * days, to years.
 *
 * Version: 0.9
 *
 * @param {Date} start        The start date, for example new Date(2010, 9, 21)
 *                            or new Date(2010, 9,21,23,45,00)
 * @param {Date} end          The end date
 * @param {int}  minimumStep  Optional. Minimum step size in milliseconds
 */
links.Timeline.StepDate = function(start, end, minimumStep) {

  // variables
  this.current = new Date();
  this._start = new Date();
  this._end = new Date();

  this.autoScale  = true;
  this.scale = links.Timeline.StepDate.SCALE.DAY;
  this.step = 1;

  // initialize the range
  this.setRange(start, end, minimumStep);
}

/// enum scale
links.Timeline.StepDate.SCALE = { MILLISECOND : 1,
                         SECOND : 2,
                         MINUTE : 3,
                         HOUR : 4,
                         DAY : 5,
                         MONTH : 6,
                         YEAR : 7};


/**
 * Set a new range
 * If minimumStep is provided, the step size is chosen as close as possible
 * to the minimumStep but larger than minimumStep. If minimumStep is not
 * provided, the scale is set to 1 DAY.
 * The minimumStep should correspond with the onscreen size of about 6 characters
 * @param {Date} start        The start date and time.
 * @param {Date} end          The end date and time.
 * @param {int}  minimumStep  Optional. Minimum step size in milliseconds
 */
links.Timeline.StepDate.prototype.setRange = function(start, end, minimumStep) {
  if (isNaN(start) || isNaN(end)) {
    //throw  "No legal start or end date in method setRange";
    return;
  }

  this._start      = (start != undefined)  ? new Date(start) : new Date();
  this._end        = (end != undefined)    ? new Date(end) : new Date();

  if (this.autoScale) {
    this.setMinimumStep(minimumStep);
  }
}

/**
 * Set the step iterator to the start date.
 */
links.Timeline.StepDate.prototype.start = function() {
  this.current = new Date(this._start);
  this.roundToMinor();
}

/**
 * Round the current date to the first minor date value
 * This must be executed once when the current date is set to start Date
 */
links.Timeline.StepDate.prototype.roundToMinor = function() {
  // round to floor
  // IMPORTANT: we have no breaks in this switch! (this is no bug)
  switch (this.scale) {
    case links.Timeline.StepDate.SCALE.YEAR:
      this.current.setFullYear(this.step * Math.floor(this.current.getFullYear() / this.step));
      this.current.setMonth(0);
    case links.Timeline.StepDate.SCALE.MONTH:        this.current.setDate(1);
    case links.Timeline.StepDate.SCALE.DAY:          this.current.setHours(0);
    case links.Timeline.StepDate.SCALE.HOUR:         this.current.setMinutes(0);
    case links.Timeline.StepDate.SCALE.MINUTE:       this.current.setSeconds(0);
    case links.Timeline.StepDate.SCALE.SECOND:       this.current.setMilliseconds(0);
    //case links.Timeline.StepDate.SCALE.MILLISECOND: // nothing to do for milliseconds
  }

  if (this.step != 1) {
    // round down to the first minor value that is a multiple of the current step size
    switch (this.scale) {
      case links.Timeline.StepDate.SCALE.MILLISECOND:  this.current.setMilliseconds(this.current.getMilliseconds() - this.current.getMilliseconds() % this.step);  break;
      case links.Timeline.StepDate.SCALE.SECOND:       this.current.setSeconds(this.current.getSeconds() - this.current.getSeconds() % this.step);  break;
      case links.Timeline.StepDate.SCALE.MINUTE:       this.current.setMinutes(this.current.getMinutes() - this.current.getMinutes() % this.step);  break;
      case links.Timeline.StepDate.SCALE.HOUR:         this.current.setHours(this.current.getHours() - this.current.getHours() % this.step);  break;
      case links.Timeline.StepDate.SCALE.DAY:          this.current.setDate((this.current.getDate()-1) - (this.current.getDate()-1) % this.step + 1);  break;
      case links.Timeline.StepDate.SCALE.MONTH:        this.current.setMonth(this.current.getMonth() - this.current.getMonth() % this.step);  break;
      case links.Timeline.StepDate.SCALE.YEAR:         this.current.setFullYear(this.current.getFullYear() - this.current.getFullYear() % this.step); break;
      default:                      break;
    }
  }
}

/**
 * Check if the end date is reached
 * @return {boolean}  true if the current date has passed the end date
 */
links.Timeline.StepDate.prototype.end = function () {
  return (this.current.getTime() > this._end.getTime());
}

/**
 * Do the next step
 */
links.Timeline.StepDate.prototype.next = function() {
  var prev = this.current.getTime();

  // Two cases, needed to prevent issues with switching daylight savings
  // (end of March and end of October)
  if (this.current.getMonth() < 6)   {
    switch (this.scale)
    {
      case links.Timeline.StepDate.SCALE.MILLISECOND:

      this.current = new Date(this.current.getTime() + this.step); break;
      case links.Timeline.StepDate.SCALE.SECOND:       this.current = new Date(this.current.getTime() + this.step * 1000); break;
      case links.Timeline.StepDate.SCALE.MINUTE:       this.current = new Date(this.current.getTime() + this.step * 1000 * 60); break;
      case links.Timeline.StepDate.SCALE.HOUR:
        this.current = new Date(this.current.getTime() + this.step * 1000 * 60 * 60);
        // in case of skipping an hour for daylight savings, adjust the hour again (else you get: 0h 5h 9h ... instead of 0h 4h 8h ...)
        var h = this.current.getHours();
        this.current.setHours(h - (h % this.step));
        break;
      case links.Timeline.StepDate.SCALE.DAY:          this.current.setDate(this.current.getDate() + this.step); break;
      case links.Timeline.StepDate.SCALE.MONTH:        this.current.setMonth(this.current.getMonth() + this.step); break;
      case links.Timeline.StepDate.SCALE.YEAR:         this.current.setFullYear(this.current.getFullYear() + this.step); break;
      default:                      break;
    }
  }
  else {
    switch (this.scale)
    {
      case links.Timeline.StepDate.SCALE.MILLISECOND:

      this.current = new Date(this.current.getTime() + this.step); break;
      case links.Timeline.StepDate.SCALE.SECOND:       this.current.setSeconds(this.current.getSeconds() + this.step); break;
      case links.Timeline.StepDate.SCALE.MINUTE:       this.current.setMinutes(this.current.getMinutes() + this.step); break;
      case links.Timeline.StepDate.SCALE.HOUR:         this.current.setHours(this.current.getHours() + this.step); break;
      case links.Timeline.StepDate.SCALE.DAY:          this.current.setDate(this.current.getDate() + this.step); break;
      case links.Timeline.StepDate.SCALE.MONTH:        this.current.setMonth(this.current.getMonth() + this.step); break;
      case links.Timeline.StepDate.SCALE.YEAR:         this.current.setFullYear(this.current.getFullYear() + this.step); break;
      default:                      break;
    }
  }

  if (this.step != 1) {
    // round down to the correct major value
    switch (this.scale) {
      case links.Timeline.StepDate.SCALE.MILLISECOND:  if(this.current.getMilliseconds() < this.step) this.current.setMilliseconds(0);  break;
      case links.Timeline.StepDate.SCALE.SECOND:       if(this.current.getSeconds() < this.step) this.current.setSeconds(0);  break;
      case links.Timeline.StepDate.SCALE.MINUTE:       if(this.current.getMinutes() < this.step) this.current.setMinutes(0);  break;
      case links.Timeline.StepDate.SCALE.HOUR:         if(this.current.getHours() < this.step) this.current.setHours(0);  break;
      case links.Timeline.StepDate.SCALE.DAY:          if(this.current.getDate() < this.step+1) this.current.setDate(1); break;
      case links.Timeline.StepDate.SCALE.MONTH:        if(this.current.getMonth() < this.step) this.current.setMonth(0);  break;
      case links.Timeline.StepDate.SCALE.YEAR:         break; // nothing to do for year
      default:                break;
    }
  }

  // safety mechanism: if current time is still unchanged, move to the end
  if (this.current.getTime() == prev) {
    this.current = new Date(this._end);
  }
}


/**
 * Get the current datetime
 * @return {Date}  current The current date
 */
links.Timeline.StepDate.prototype.getCurrent = function() {
  return this.current;
}

/**
 * Set a custom scale. Autoscaling will be disabled.
 * For example setScale(SCALE.MINUTES, 5) will result
 * in minor steps of 5 minutes, and major steps of an hour.
 *
 * @param {Step.SCALE} newScale  A scale. Choose from SCALE.MILLISECOND,
 *                               SCALE.SECOND, SCALE.MINUTE, SCALE.HOUR,
 *                               SCALE.DAY, SCALE.MONTH, SCALE.YEAR.
 * @param {int}        newStep   A step size, by default 1. Choose for
 *                               example 1, 2, 5, or 10.
 */
links.Timeline.StepDate.prototype.setScale = function(newScale, newStep) {
  this.scale = newScale;

  if (newStep > 0)
    this.step = newStep;

  this.autoScale = false;
}

/**
 * Enable or disable autoscaling
 * @param {boolean} enable  If true, autoascaling is set true
 */
links.Timeline.StepDate.prototype.setAutoScale = function (enable) {
  this.autoScale = enable;
}


/**
 * Automatically determine the scale that bests fits the provided minimum step
 * @param {int} minimumStep  The minimum step size in milliseconds
 */
links.Timeline.StepDate.prototype.setMinimumStep = function(minimumStep) {
  if (minimumStep == undefined)
    return;

  var stepYear       = (1000 * 60 * 60 * 24 * 30 * 12);
  var stepMonth      = (1000 * 60 * 60 * 24 * 30);
  var stepDay        = (1000 * 60 * 60 * 24);
  var stepHour       = (1000 * 60 * 60);
  var stepMinute     = (1000 * 60);
  var stepSecond     = (1000);
  var stepMillisecond= (1);

  // find the smallest step that is larger than the provided minimumStep
  if (stepYear*1000 > minimumStep)        {this.scale = links.Timeline.StepDate.SCALE.YEAR;        this.step = 1000;}
  if (stepYear*500 > minimumStep)         {this.scale = links.Timeline.StepDate.SCALE.YEAR;        this.step = 500;}
  if (stepYear*100 > minimumStep)         {this.scale = links.Timeline.StepDate.SCALE.YEAR;        this.step = 100;}
  if (stepYear*50 > minimumStep)          {this.scale = links.Timeline.StepDate.SCALE.YEAR;        this.step = 50;}
  if (stepYear*10 > minimumStep)          {this.scale = links.Timeline.StepDate.SCALE.YEAR;        this.step = 10;}
  if (stepYear*5 > minimumStep)           {this.scale = links.Timeline.StepDate.SCALE.YEAR;        this.step = 5;}
  if (stepYear > minimumStep)             {this.scale = links.Timeline.StepDate.SCALE.YEAR;        this.step = 1;}
  if (stepMonth*3 > minimumStep)          {this.scale = links.Timeline.StepDate.SCALE.MONTH;       this.step = 3;}
  if (stepMonth > minimumStep)            {this.scale = links.Timeline.StepDate.SCALE.MONTH;       this.step = 1;}
  if (stepDay*5 > minimumStep)            {this.scale = links.Timeline.StepDate.SCALE.DAY;         this.step = 5;}
  if (stepDay*2 > minimumStep)            {this.scale = links.Timeline.StepDate.SCALE.DAY;         this.step = 2;}
  if (stepDay > minimumStep)              {this.scale = links.Timeline.StepDate.SCALE.DAY;         this.step = 1;}
  if (stepHour*4 > minimumStep)           {this.scale = links.Timeline.StepDate.SCALE.HOUR;        this.step = 4;}
  if (stepHour > minimumStep)             {this.scale = links.Timeline.StepDate.SCALE.HOUR;        this.step = 1;}
  if (stepMinute*15 > minimumStep)        {this.scale = links.Timeline.StepDate.SCALE.MINUTE;      this.step = 15;}
  if (stepMinute*10 > minimumStep)        {this.scale = links.Timeline.StepDate.SCALE.MINUTE;      this.step = 10;}
  if (stepMinute*5 > minimumStep)         {this.scale = links.Timeline.StepDate.SCALE.MINUTE;      this.step = 5;}
  if (stepMinute > minimumStep)           {this.scale = links.Timeline.StepDate.SCALE.MINUTE;      this.step = 1;}
  if (stepSecond*15 > minimumStep)        {this.scale = links.Timeline.StepDate.SCALE.SECOND;      this.step = 15;}
  if (stepSecond*10 > minimumStep)        {this.scale = links.Timeline.StepDate.SCALE.SECOND;      this.step = 10;}
  if (stepSecond*5 > minimumStep)         {this.scale = links.Timeline.StepDate.SCALE.SECOND;      this.step = 5;}
  if (stepSecond > minimumStep)           {this.scale = links.Timeline.StepDate.SCALE.SECOND;      this.step = 1;}
  if (stepMillisecond*200 > minimumStep)  {this.scale = links.Timeline.StepDate.SCALE.MILLISECOND; this.step = 200;}
  if (stepMillisecond*100 > minimumStep)  {this.scale = links.Timeline.StepDate.SCALE.MILLISECOND; this.step = 100;}
  if (stepMillisecond*50 > minimumStep)   {this.scale = links.Timeline.StepDate.SCALE.MILLISECOND; this.step = 50;}
  if (stepMillisecond*10 > minimumStep)   {this.scale = links.Timeline.StepDate.SCALE.MILLISECOND; this.step = 10;}
  if (stepMillisecond*5 > minimumStep)    {this.scale = links.Timeline.StepDate.SCALE.MILLISECOND; this.step = 5;}
  if (stepMillisecond > minimumStep)      {this.scale = links.Timeline.StepDate.SCALE.MILLISECOND; this.step = 1;}
}

/**
 * Snap a date to a rounded value. The snap intervals are dependent on the
 * current scale and step.
 * @param {Date} date   the date to be snapped
 */
links.Timeline.StepDate.prototype.snap = function(date) {
  if (this.scale == links.Timeline.StepDate.SCALE.YEAR) {
    var year = date.getFullYear() + Math.round(date.getMonth() / 12);
    date.setFullYear(Math.round(year / this.step) * this.step);
    date.setMonth(0);
    date.setDate(0);
    date.setHours(0);
    date.setMinutes(0);
    date.setSeconds(0);
    date.setMilliseconds(0);
  }
  else if (this.scale == links.Timeline.StepDate.SCALE.MONTH) {
    if (date.getDate() > 15) {
      date.setDate(1);
      date.setMonth(date.getMonth() + 1);
      // important: first set Date to 1, after that change the month.
    }
    else {
      date.setDate(1);
    }

    date.setHours(0);
    date.setMinutes(0);
    date.setSeconds(0);
    date.setMilliseconds(0);
  }
  else if (this.scale == links.Timeline.StepDate.SCALE.DAY) {
    switch (this.step) {
      case 5:
      case 2:
        date.setHours(Math.round(date.getHours() / 24) * 24); break;
      default:
        date.setHours(Math.round(date.getHours() / 12) * 12); break;
    }
    date.setMinutes(0);
    date.setSeconds(0);
    date.setMilliseconds(0);
  }
  else if (this.scale == links.Timeline.StepDate.SCALE.HOUR) {
    switch (this.step) {
      case 4:
        date.setMinutes(Math.round(date.getMinutes() / 60) * 60); break;
      default:
        date.setMinutes(Math.round(date.getMinutes() / 30) * 30); break;
    }
    date.setSeconds(0);
    date.setMilliseconds(0);
  } else if (this.scale == links.Timeline.StepDate.SCALE.MINUTE) {
    switch (this.step) {
      case 15:
      case 10:
        date.setMinutes(Math.round(date.getMinutes() / 5) * 5);
        date.setSeconds(0);
        break;
      case 5:
        date.setSeconds(Math.round(date.getSeconds() / 60) * 60); break;
      default:
        date.setSeconds(Math.round(date.getSeconds() / 30) * 30); break;
    }
    date.setMilliseconds(0);
  }
  else if (this.scale == links.Timeline.StepDate.SCALE.SECOND) {
    switch (this.step) {
      case 15:
      case 10:
        date.setSeconds(Math.round(date.getSeconds() / 5) * 5);
        date.setMilliseconds(0);
        break;
      case 5:
        date.setMilliseconds(Math.round(date.getMilliseconds() / 1000) * 1000); break;
      default:
        date.setMilliseconds(Math.round(date.getMilliseconds() / 500) * 500); break;
    }
  }
  else if (this.scale == links.Timeline.StepDate.SCALE.MILLISECOND) {
    var step = this.step > 5 ? this.step / 2 : 1;
    date.setMilliseconds(Math.round(date.getMilliseconds() / step) * step);
  }
}

/**
 * Check if the current step is a major step (for example when the step
 * is DAY, a major step is each first day of the MONTH)
 * @return true if current date is major, else false.
 */
links.Timeline.StepDate.prototype.isMajor = function() {
  switch (this.scale)
  {
    case links.Timeline.StepDate.SCALE.MILLISECOND:
      return (this.current.getMilliseconds() == 0);
    case links.Timeline.StepDate.SCALE.SECOND:
      return (this.current.getSeconds() == 0);
    case links.Timeline.StepDate.SCALE.MINUTE:
      return (this.current.getHours() == 0) && (this.current.getMinutes() == 0);
      // Note: this is no bug. Major label is equal for both minute and hour scale
    case links.Timeline.StepDate.SCALE.HOUR:
      return (this.current.getHours() == 0);
    case links.Timeline.StepDate.SCALE.DAY:
      return (this.current.getDate() == 1);
    case links.Timeline.StepDate.SCALE.MONTH:
      return (this.current.getMonth() == 0);
    case links.Timeline.StepDate.SCALE.YEAR:
      return false
    default:
      return false;
  }
}


/**
 * Returns formatted text for the minor axislabel, depending on the current
 * date and the scale. For example when scale is MINUTE, the current time is
 * formatted as "hh:mm".
 * @param {Date}       optional custom date. if not provided, current date is taken
 * @return {string}    minor axislabel
 */
links.Timeline.StepDate.prototype.getLabelMinor = function(date) {
  var MONTHS_SHORT = new Array("Jan", "Feb", "Mar",
                                "Apr", "May", "Jun",
                                "Jul", "Aug", "Sep",
                                "Oct", "Nov", "Dec");

  if (date == undefined) {
    date = this.current;
  }

  switch (this.scale)
  {
    case links.Timeline.StepDate.SCALE.MILLISECOND:  return String(date.getMilliseconds());
    case links.Timeline.StepDate.SCALE.SECOND:       return String(date.getSeconds());
    case links.Timeline.StepDate.SCALE.MINUTE:       return this.addZeros(date.getHours(), 2) + ":" +
                                                       this.addZeros(date.getMinutes(), 2);
    case links.Timeline.StepDate.SCALE.HOUR:         return this.addZeros(date.getHours(), 2) + ":" +
                                                       this.addZeros(date.getMinutes(), 2);
    case links.Timeline.StepDate.SCALE.DAY:          return String(date.getDate());
    case links.Timeline.StepDate.SCALE.MONTH:        return MONTHS_SHORT[date.getMonth()];   // month is zero based
    case links.Timeline.StepDate.SCALE.YEAR:         return String(date.getFullYear());
    default:                                         return "";
  }
}


/**
 * Returns formatted text for the major axislabel, depending on the current
 * date and the scale. For example when scale is MINUTE, the major scale is
 * hours, and the hour will be formatted as "hh".
 * @param {Date}       optional custom date. if not provided, current date is taken
 * @return {string}    major axislabel
 */
links.Timeline.StepDate.prototype.getLabelMajor = function(date) {
  var MONTHS = new Array("January", "February", "March",
                         "April", "May", "June",
                         "July", "August", "September",
                         "October", "November", "December");
  var DAYS = new Array("Sunday", "Monday", "Tuesday",
                       "Wednesday", "Thursday", "Friday", "Saturday");

  if (date == undefined) {
    date = this.current;
  }

  switch (this.scale) {
    case links.Timeline.StepDate.SCALE.MILLISECOND:
      return  this.addZeros(date.getHours(), 2) + ":" +
              this.addZeros(date.getMinutes(), 2) + ":" +
              this.addZeros(date.getSeconds(), 2);
    case links.Timeline.StepDate.SCALE.SECOND:
      return  date.getDate() + " " +
              MONTHS[date.getMonth()] + " " +
              this.addZeros(date.getHours(), 2) + ":" +
              this.addZeros(date.getMinutes(), 2);
    case links.Timeline.StepDate.SCALE.MINUTE:
      return  DAYS[date.getDay()] + " " +
              date.getDate() + " " +
              MONTHS[date.getMonth()] + " " +
              date.getFullYear();
    case links.Timeline.StepDate.SCALE.HOUR:
      return  DAYS[date.getDay()] + " " +
              date.getDate() + " " +
              MONTHS[date.getMonth()] + " " +
              date.getFullYear();
    case links.Timeline.StepDate.SCALE.DAY:
      return  MONTHS[date.getMonth()] + " " +
              date.getFullYear();
    case links.Timeline.StepDate.SCALE.MONTH:
      return String(date.getFullYear());
    default:
      return "";
  }
}

/**
 * Add leading zeros to the given value to match the desired length.
 * For example addZeros(123, 5) returns "00123"
 * @param {int} value   A value
 * @param {int} len     Desired final length
 * @return {string}     value with leading zeros
 */
links.Timeline.StepDate.prototype.addZeros = function(value, len) {
  var str = "" + value;
  while (str.length < len) {
    str = "0" + str;
  }
  return str;
}



/** ------------------------------------------------------------------------ **/

/**
 * Image Loader service.
 * can be used to get a callback when a certain image is loaded
 *
 */
links.imageloader = (function () {
  var urls = {};  // the loaded urls
  var callbacks = {}; // the urls currently being loaded. Each key contains
                      // an array with callbacks

  /**
   * Check if an image url is loaded
   * @param {String} url
   * @return {boolean} loaded   True when loaded, false when not loaded
   *                            or when being loaded
   */
  function isLoaded (url) {
    if (urls[url] == true) {
      return true;
    }

    var image = new Image();
    image.src = url;
    if (image.complete) {
      return true;
    }

    return false;
  };


  /**
   * Check if an image url is being loaded
   * @param {String} url
   * @return {boolean} loading   True when being loaded, false when not loading
   *                             or when already loaded
   */
  function isLoading (url) {
    return (callbacks[url] != undefined);
  }

  /**
   * Load given image url
   * @param {String} url
   * @param {function} callback
   * @param {boolean} sendCallbackWhenAlreadyLoaded  optional
   */
  function load (url, callback, sendCallbackWhenAlreadyLoaded) {
    if (sendCallbackWhenAlreadyLoaded == undefined) {
      sendCallbackWhenAlreadyLoaded = true;
    }

    if (isLoaded(url)) {
      if (sendCallbackWhenAlreadyLoaded) {
        callback(url);
      }
      return;
    }

    if (isLoading(url) && !sendCallbackWhenAlreadyLoaded) {
      return;
    }

    var c = callbacks[url];
    if (!c) {
      var image = new Image();
      image.src = url;

      c = [];
      callbacks[url] = c;

      image.onload = function (event) {
        urls[url] = true;
        delete callbacks[url];

        for (var i = 0; i < c.length; i++) {
          c[i](url);
        }
      }
    }

    if (c.indexOf(callback) == -1) {
      c.push(callback);
    }
  };

  return {
    'isLoaded': isLoaded,
    'isLoading': isLoading,
    'load': load
  };
})();


/** ------------------------------------------------------------------------ **/


/**
 * Add and event listener. Works for all browsers
 * @param {DOM Element} element    An html element
 * @param {string}      action     The action, for example "click",
 *                                 without the prefix "on"
 * @param {function}    listener   The callback function to be executed
 * @param {boolean}     useCapture
 */
links.Timeline.addEventListener = function (element, action, listener, useCapture) {
  if (element.addEventListener) {
    if (useCapture === undefined)
      useCapture = false;

    if (action === "mousewheel" && navigator.userAgent.indexOf("Firefox") >= 0) {
      action = "DOMMouseScroll";  // For Firefox
    }

    element.addEventListener(action, listener, useCapture);
  } else {
    element.attachEvent("on" + action, listener);  // IE browsers
  }
};

/**
 * Remove an event listener from an element
 * @param {DOM element}  element   An html dom element
 * @param {string}       action    The name of the event, for example "mousedown"
 * @param {function}     listener  The listener function
 * @param {boolean}      useCapture
 */
links.Timeline.removeEventListener = function(element, action, listener, useCapture) {
  if (element.removeEventListener) {
    // non-IE browsers
    if (useCapture === undefined)
      useCapture = false;

    if (action === "mousewheel" && navigator.userAgent.indexOf("Firefox") >= 0) {
      action = "DOMMouseScroll";  // For Firefox
    }

    element.removeEventListener(action, listener, useCapture);
  } else {
    // IE browsers
    element.detachEvent("on" + action, listener);
  }
};


/**
 * Get HTML element which is the target of the event
 * @param {MouseEvent} event
 * @return {HTML DOM} target element
 */
links.Timeline.getTarget = function (event) {
  // code from http://www.quirksmode.org/js/events_properties.html
  if (!event) {
    var event = window.event;
  }

  var target;

  if (event.target) {
    target = event.target;
  }
  else if (event.srcElement) {
    target = event.srcElement;
  }

  if (target.nodeType !== undefined && target.nodeType == 3) {
    // defeat Safari bug
    target = target.parentNode;
  }

  return target;
}

/**
 * Stop event propagation
 */
links.Timeline.stopPropagation = function (event) {
  if (!event)
    var event = window.event;

  if (event.stopPropagation) {
    event.stopPropagation();  // non-IE browsers
  }
  else {
    event.cancelBubble = true;  // IE browsers
  }
}


/**
 * Cancels the event if it is cancelable, without stopping further propagation of the event.
 */
links.Timeline.preventDefault = function (event) {
  if (!event)
    var event = window.event;

  if (event.preventDefault) {
    event.preventDefault();  // non-IE browsers
  }
  else {
    event.returnValue = false;  // IE browsers
  }
}


/**
 * Retrieve the absolute left value of a DOM element
 * @param {DOM element} elem    A dom element, for example a div
 * @return {number} left        The absolute left position of this element
 *                              in the browser page.
 */
links.Timeline.getAbsoluteLeft = function(elem)
{
  var left = 0;
  while( elem != null ) {
    left += elem.offsetLeft;
    left -= elem.scrollLeft;
    elem = elem.offsetParent;
  }
  if (!document.body.scrollLeft && window.pageXOffset) {
      // FF
      left -= window.pageXOffset;
  }
  return left;
}

/**
 * Retrieve the absolute top value of a DOM element
 * @param {DOM element} elem    A dom element, for example a div
 * @return {number} top        The absolute top position of this element
 *                              in the browser page.
 */
links.Timeline.getAbsoluteTop = function(elem)
{
  var top = 0;
  while( elem != null ) {
    top += elem.offsetTop;
    top -= elem.scrollTop;
    elem = elem.offsetParent;
  }
  if (!document.body.scrollTop && window.pageYOffset) {
      // FF
      top -= window.pageYOffset;
  }
  return top;
}

/**
 * Check if given object is a Javascript Array
 * @param {any type} obj
 * @return {Boolean} isArray    true if the given object is an array
 */
// See http://stackoverflow.com/questions/2943805/javascript-instanceof-typeof-in-gwt-jsni
links.Timeline.isArray = function (obj) {
  if (obj instanceof Array) {
    return true;
  }
  return (Object.prototype.toString.call(obj) === '[object Array]');
}

      var timeline;
      var data;

      // Called when the Visualization API is loaded.
config.macros.drawVisualization = {};
config.macros.drawVisualization.handler = function (place,macroName,params,wikifier,paramString,tiddler){
        // specify options
        var options = {
          'width':  '100%',
          'height': '500px',
          'editable': false,   // enable dragging and editing events
          'style': 'box',
          'showNavigation': true
        };

        // Instantiate our timeline object.
        timeline = new links.Timeline(place);

        function onRangeChanged(properties) {
          document.getElementById('info').innerHTML += 'rangechanged ' +
            properties.start + ' - ' + properties.end + '<br>';
        };

        // attach an event listener using the links events handler
        links.events.addListener(timeline, 'rangechanged', onRangeChanged);

        // Draw our timeline with the created data and options
//        Examples:
//        timeline.draw(data1, options);
//	  timeline.addItem({'start': new Date(2012,01,24), 'end': new Date(2012,2,24), 'content': '<div style="background-color:#00FFFF; border:0px solid green;padding:0px;">Possible visibile</div>',             'group': 'T-12-03' });
	params = paramString.parseParams("anon",null,true,false,false);
//	timeline.draw([], options);
	var title = getParam(params,"anon","");
	if(title == "" && tiddler instanceof Tiddler)
		title = tiddler.title;
	var sortby = getParam(params,"sortBy",false);
	var tagged = store.getTaggedTiddlers(title,sortby);
	var t;
	for(t=0; t<tagged.length; t++) {
		var StartDate = store.getTiddlerSlice(tagged[t].title,"StartDate") ;
		var SDDate = new Date(StartDate);
		var EndDate = store.getTiddlerSlice(tagged[t].title,"EndDate") ;
		var EDDate = new Date(EndDate);
		var BarColor = store.getTiddlerSlice(tagged[t].title,"Color") ;
		var BarText= store.getTiddlerSlice(tagged[t].title,"Text") ;
		var BarGroup= store.getTiddlerSlice(tagged[t].title,"Group") ;
		var CTcontent = '<div style="background-color:' + BarColor + '; border:0px solid ' + BarColor + '; padding:0px;" data-tiddler=\"'+tagged[t].title+'\">' + BarText + '</div>';

		if ((Date.parse(StartDate) || 0) > 0) {
			if ((Date.parse(EndDate) || 0) > 0)
				{
				timeline.addItem({'start': SDDate, 'end': EDDate , 'content': CTcontent, 'group': BarGroup});
				}
			else
				{
				timeline.addItem({'start': SDDate, 'content': CTcontent, 'group': BarGroup});
				}
		}
	}
	timeline.setVisibleChartRangeAuto();
	links.events.addListener(timeline, 'select', onselect);
}
function onselect() {
  var sel = timeline.getSelection();
  if (sel.length) {
    if (sel[0].row != undefined) {
      var row = sel[0].row;
	var patt=/(data-tiddler\s*=\s*")(.*?)(")/g;
	var myResult=patt.exec(timeline.getItem(row).content);
	story.displayTiddler("top", myResult[2], DEFAULT_VIEW_TEMPLATE, true);
    }
  }
}

//}}}
/***
|''Name:''|CHAPTimelinePlugin|
|''Version:''|0.4 (2012-06-01)|
|''Author:''|AntonJ, based on code from Almende at http://chap.almende.com/timeline.  All rights are theirs where appropriate.  My bit is licenced under the GPL|
|''Adapted By:''||
|''Type:''|Plugin|
|''Requires:''|Nil, although can be manipulated using ForEachTiddlerPlugin|
!Description
This Plugin implements the CHAP Links Timeline
!Usage
Just install the plugin and tag with systemConfig. Put the following in the tiddler you wish to contain the timline.
{{{
<<drawVisualization DataTag>>
<html><div id='mytimeline'></div></html>
}}}
where "DataTag" is the tag allocated to tiddlers which contain the data for the timline events.

Create tiddlers to contain your events using the tag you selected above (eg DataTag).  Put the following slices in the body of the tiddlers (eg date provided):

{{{
StartDate:2012-04-23
EndDate:2012-12-01
Color:Orchid
Text:My Task
Group:Group 1
}}}
I use ISO ISO date format (YYYY-MM-DD) however other formats may work.
Color is one of the standard HTML colors.
Group may be used to put multiple bars on one line.

!Revision History
*v0.4		- Added ability to add single date events.
*v0.3 	- Changed from using DataTiddlerPlugin to using slices.
		- Improved date handling.
* Just started

!Code
***/

// // Blah
//{{{
/**
 * @file timeline.js
 *
 * @brief
 * The Timeline is an interactive visualization chart to visualize events in
 * time, having a start and end date.
 * You can freely move and zoom in the timeline by dragging
 * and scrolling in the Timeline. Items are optionally dragable. The time
 * scale on the axis is adjusted automatically, and supports scales ranging
 * from milliseconds to years.
 *
 * Timeline is part of the CHAP Links library.
 *
 * Timeline is tested on Firefox 3.6, Safari 5.0, Chrome 6.0, Opera 10.6, and
 * Internet Explorer 6+.
 *
 * @license
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy
 * of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 *
 * Copyright (c) 2011-2012 Almende B.V.
 *
 * @author 	Jos de Jong, <jos@almende.org>
 * @date    2012-03-22
 */


/*
 * TODO
 *
 * Add methods deleteItem, addItem, changeItem to the GWT wrapper
 * Add moving items from one group to another
 * Add options for a minimum and maximum zoom level
 * Add zooming with pinching on Android
 *
 * Bug: neglect items when they have no valid start/end, instead of throwing an error
 * Bug: Pinching on ipad does not work very well, sometimes the page will zoom when pinching vertically
 * Bug: cannot set max width for an item, like div.timeline-event-content {white-space: normal; max-width: 100px;}
 * Bug on IE in Quirks mode. When you have groups, and delete an item, the groups become invisible
 *
 */

/**
 * Declare a unique namespace for CHAP's Common Hybrid Visualisation Library,
 * "links"
 */
if (typeof links === 'undefined') {
  links = {};
  // important: do not use var, as "var links = {};" will overwrite
  //            the existing links variable value with undefined in IE8, IE7.
}


/**
 * Ensure the variable google exists
 */
if (typeof google === 'undefined') {
  google = undefined;
  // important: do not use var, as "var google = undefined;" will overwrite
  //            the existing google variable value with undefined in IE8, IE7.
}


/**
 * @class Timeline
 * The timeline is a visualization chart to visualize events in time.
 *
 * The timeline is developed in javascript as a Google Visualization Chart.
 *
 * @param {dom_element} container   The DOM element in which the Timeline will
 *                                  be created. Normally a div element.
 */
links.Timeline = function(container) {
  // create variables and set default values
  this.dom = {};
  this.conversion = {};
  this.eventParams = {}; // stores parameters for mouse events
  this.groups = [];
  this.groupIndexes = {};
  this.items = [];
  this.selection = undefined; // stores index and item which is currently selected

  this.listeners = {}; // event listener callbacks

  // Initialize sizes.
  // Needed for IE (which gives an error when you try to set an undefined
  // value in a style)
  this.size = {
    'actualHeight': 0,
    'axis': {
      'characterMajorHeight': 0,
      'characterMajorWidth': 0,
      'characterMinorHeight': 0,
      'characterMinorWidth': 0,
      'height': 0,
      'labelMajorTop': 0,
      'labelMinorTop': 0,
      'line': 0,
      'lineMajorWidth': 0,
      'lineMinorHeight': 0,
      'lineMinorTop': 0,
      'lineMinorWidth': 0,
      'top': 0
    },
    'contentHeight': 0,
    'contentLeft': 0,
    'contentWidth': 0,
    'dataChanged': false,
    'frameHeight': 0,
    'frameWidth': 0,
    'groupsLeft': 0,
    'groupsWidth': 0,
    'items': {
      'top': 0
    }
  };

  this.dom.container = container;

  this.options = {
    'width': "100%",
    'height': "auto",
    'minHeight': 0,       // minimal height in pixels
    'autoHeight': true,

    'eventMargin': 10,    // minimal margin between events
    'eventMarginAxis': 20, // minimal margin beteen events and the axis
    'dragAreaWidth': 10, // pixels

    'moveable': true,
    'zoomable': true,
    'selectable': true,
    'editable': false,
    'snapEvents': true,

    'showCurrentTime': true, // show a red bar displaying the current time
    'showCustomTime': false, // show a blue, draggable bar displaying a custom time
    'showMajorLabels': true,
    'showNavigation': false,
    'showButtonAdd': true,
    'groupsOnRight': false,
    'axisOnTop': false,
    'stackEvents': true,
    'animate': true,
    'animateZoom': true,
    'style': 'box'
  };

  this.clientTimeOffset = 0;    // difference between client time and the time
                                // set via Timeline.setCurrentTime()
  var dom = this.dom;

  // remove all elements from the container element.
  while (dom.container.hasChildNodes()) {
    dom.container.removeChild(dom.container.firstChild);
  }

  // create a step for drawing the axis
  this.step = new links.Timeline.StepDate();

  // initialize data
  this.data = [];
  this.firstDraw = true;

  // date interval must be initialized
  this.setVisibleChartRange(undefined, undefined, false);

  // create all DOM elements
  this.redrawFrame();

  // Internet Explorer does not support Array.indexof,
  // so we define it here in that case
  // http://soledadpenades.com/2007/05/17/arrayindexof-in-internet-explorer/
	if(!Array.prototype.indexOf) {
    Array.prototype.indexOf = function(obj){
      for(var i = 0; i < this.length; i++){
        if(this[i] == obj){
          return i;
        }
      }
      return -1;
    }
	}

  // fire the ready event
  this.trigger('ready');
}


/**
 * Main drawing logic. This is the function that needs to be called
 * in the html page, to draw the timeline.
 *
 * A data table with the events must be provided, and an options table.
 *
 * @param {DataTable}      data    The data containing the events for the timeline.
 *                                 Object DataTable is defined in
 *                                 google.visualization.DataTable
 * @param {name/value map} options A name/value map containing settings for the
 *                                 timeline. Optional.
 */
links.Timeline.prototype.draw = function(data, options) {
  if (options) {
    // retrieve parameter values
    for (var i in options) {
      if (options.hasOwnProperty(i)) {
        this.options[i] = options[i];
      }
    }
  }
  this.options.autoHeight = (this.options.height === "auto");

  // read the data
  this.setData(data);

  // set timer range. this will also redraw the timeline
  if (options && options.start && options.end) {
    this.setVisibleChartRange(options.start, options.end);
  }
  else if (this.firstDraw) {
    this.setVisibleChartRangeAuto();
  }

  this.firstDraw = false;
}

/**
 * Set data for the timeline
 * @param {DataTable or JSON array} data
 */
links.Timeline.prototype.setData = function(data) {
  // unselect any previously selected item
  this.unselectItem();

  if (!data) {
    data = [];
  }

  this.items = [];
  this.data = data;
  var items = this.items;
  var options = this.options;

  // create groups from the data
  this.setGroups(data);

  if (google && google.visualization &&
      data instanceof google.visualization.DataTable) {
    // read DataTable
    var hasGroups = (data.getNumberOfColumns() > 3);
    for (var row = 0, rows = data.getNumberOfRows(); row < rows; row++) {
      items.push(this.createItem({
        'start': data.getValue(row, 0),
        'end': data.getValue(row, 1),
        'content': data.getValue(row, 2),
        'group': (hasGroups ? data.getValue(row, 3) : undefined)
      }));
    }
  }
  else if (links.Timeline.isArray(data)) {
    // read JSON array
    for (var row = 0, rows = data.length; row < rows; row++) {
      var itemData = data[row]
      var item = this.createItem(itemData);
      items.push(item);
    }
  }
  else {
    throw "Unknown data type. DataTable or Array expected.";
  }

  // set a flag to force the recalcSize method to recalculate the
  // heights and widths of the events
  this.size.dataChanged = true;
  this.redrawFrame();      // create the items for the new data
  this.recalcSize();       // position the items
  this.stackEvents(false);
  this.redrawFrame();      // redraw the items on the final positions
  this.size.dataChanged = false;
}

/**
 * Set the groups available in the given dataset
 * @param {DataTable or JSON array} data
 */
links.Timeline.prototype.setGroups = function (data) {
  this.deleteGroups();
  var groups = this.groups;
  var groupIndexes = this.groupIndexes;

  if (google && google.visualization &&
      data instanceof google.visualization.DataTable) {
    // get groups from DataTable
    var hasGroups = (data.getNumberOfColumns() > 3);
    if (hasGroups) {
      var groupNames = data.getDistinctValues(3);
      for (var i = 0, iMax = groupNames.length; i < iMax; i++) {
        this.addGroup(groupNames[i]);
      }
    }
  }
  else if (links.Timeline.isArray(data)){
    // get groups from JSON Array
    for (var i = 0, iMax = data.length; i < iMax; i++) {
      var row = data[i],
        group = row.group;
      if (group) {
        this.addGroup(group);
      }
    }
  }
  else {
    throw 'Unknown data type. DataTable or Array expected.';
  }
}


/**
 * Return the original data table.
 * @param {Google DataTable or Array} data
 */
links.Timeline.prototype.getData = function  () {
  return this.data;
}


/**
 * Update the original data with changed start, end or group.
 *
 * @param {Number} index
 * @param {Object} values   An object containing some of the following parameters:
 *                          {Date} start,
 *                          {Date} end,
 *                          {String} content,
 *                          {String} group
 */
links.Timeline.prototype.updateData = function  (index, values) {
  var data = this.data;

  if (google && google.visualization &&
      data instanceof google.visualization.DataTable) {
    // update the original google DataTable
    var missingRows = (index + 1) - data.getNumberOfRows();
    if (missingRows > 0) {
      data.addRows(missingRows);
    }

    if (values.start) {
      data.setValue(index, 0, values.start);
    }
    if (values.end) {
      data.setValue(index, 1, values.end);
    }
    if (values.content) {
      data.setValue(index, 2, values.content);
    }
    if (values.group && data.getNumberOfColumns() > 3) {
      // TODO: append a column when needed?
      data.setValue(index, 3, values.group);
    }
  }
  else if (links.Timeline.isArray(data)) {
    // update the original JSON table
    var row = data[index];
    if (row == undefined) {
      row = {};
      data[index] = row;
    }

    if (values.start) {
      row.start = values.start;
    }
    if (values.end) {
      row.end = values.end;
    }
    if (values.content) {
      row.content = values.content;
    }
    if (values.group) {
      row.group = values.group;
    }
  }
  else {
    throw "Cannot update data, unknown type of data";
  }
}

/**
 * Find the item index from a given HTML element
 * If no item index is found, undefined is returned
 * @param {HTML DOM element} element
 * @return {Number} index
 */
links.Timeline.prototype.getItemIndex = function(element) {
  var e = element,
    dom = this.dom,
    items = this.items,
    index = undefined;

  // try to find the frame where the items are located in
  while (e.parentNode && e.parentNode !== dom.items.frame) {
    e = e.parentNode;
  }

  if (e.parentNode === dom.items.frame) {
    // yes! we have found the parent element of all items
    // retrieve its id from the array with items
    for (var i = 0, iMax = items.length; i < iMax; i++) {
      if (items[i].dom === e) {
        index = i;
        break;
      }
    }
  }

  return index;
}

/**
 * Set a new size for the timeline
 * @param {string} width   Width in pixels or percentage (for example "800px"
 *                         or "50%")
 * @param {string} height  Height in pixels or percentage  (for example "400px"
 *                         or "30%")
 */
links.Timeline.prototype.setSize = function(width, height) {
  if (width) {
    this.options.width = width;
    this.dom.frame.style.width = width;
  }
  if (height) {
    this.options.height = height;
    this.options.autoHeight = (this.options.height === "auto");
    if (height !==  "auto" ) {
      this.dom.frame.style.height = height;
    }
  }

  this.recalcSize();
  this.stackEvents(false);
  this.redrawFrame();
}


/**
 * Set a new value for the visible range int the timeline.
 * Set start to null to include everything from the earliest date to end.
 * Set end to null to include everything from start to the last date.
 * Example usage:
 *    myTimeline.setVisibleChartRange(new Date("2010-08-22"),
 *                                    new Date("2010-09-13"));
 * @param {Date}   start     The start date for the timeline. optional
 * @param {Date}   end       The end date for the timeline. optional
 * @param {boolean} redraw   Optional. If true (default) the Timeline is
 *                           directly redrawn
 */
links.Timeline.prototype.setVisibleChartRange = function(start, end, redraw) {
  if (start != null) {
    this.start = new Date(start);
  } else {
    // default of 3 days ago
    this.start = new Date();
    this.start.setDate(this.start.getDate() - 3);
  }

  if (end != null) {
    this.end = new Date(end);
  } else {
    // default of 4 days ahead
    this.end = new Date();
    this.end.setDate(this.end.getDate() + 4);
  }

  // prevent start Date <= end Date
  if (this.end.valueOf() <= this.start.valueOf()) {
    this.end = new Date(this.start);
    this.end.setDate(this.end.getDate() + 7);
  }

  if (redraw == undefined || redraw == true) {
    this.recalcSize();
    this.stackEvents(false);
    this.redrawFrame();
  }
  else {
    this.recalcConversion();
  }
}


/**
 * Change the visible chart range such that all items become visible
 */
links.Timeline.prototype.setVisibleChartRangeAuto = function() {
  var items = this.items;
    startMin = undefined, // long value of a data
    endMax = undefined;   // long value of a data

  // find earliest start date from the data
  for (var i = 0, iMax = items.length; i < iMax; i++) {
    var item = items[i],
      start = item.start ? item.start.valueOf() : undefined,
      end = item.end ? item.end.valueOf() : start;

    if (startMin !== undefined && start !== undefined) {
      startMin = Math.min(startMin, start);
    }
    else {
      startMin = start;
    }
    if (endMax !== undefined && end !== undefined) {
      endMax = Math.max(endMax, end);
    }
    else {
      endMax = end;
    }
  }

  if (startMin !== undefined && endMax !== undefined) {
    // zoom out 5% such that you have a little white space on the left and right
    var center = (endMax + startMin) / 2,
      diff = (endMax - startMin);
    startMin = startMin - diff * 0.05;
    endMax = endMax + diff * 0.05;

    // adjust the start and end date
    this.setVisibleChartRange(new Date(startMin), new Date(endMax));
  }
  else {
    this.setVisibleChartRange(undefined, undefined);
  }
}

/**
 * Adjust the visible range such that the current time is located in the center
 * of the timeline
 */
links.Timeline.prototype.setVisibleChartRangeNow = function() {
  var now = new Date();

  var diff = (this.end.getTime() - this.start.getTime());

  var startNew = new Date(now.getTime() - diff/2);
  var endNew = new Date(startNew.getTime() + diff);
  this.setVisibleChartRange(startNew, endNew);
}


/**
 * Retrieve the current visible range in the timeline.
 * @return {Object} An object with start and end properties
 */
links.Timeline.prototype.getVisibleChartRange = function() {
  var range = {
    'start': new Date(this.start),
    'end': new Date(this.end)
  };
  return range;
}


/**
 * Redraw the timeline. This needs to be executed after the start and/or
 * end time are changed, or when data is added or removed dynamically.
 */
links.Timeline.prototype.redrawFrame = function() {
  var dom = this.dom,
    options = this.options,
    size = this.size;

  if (!dom.frame) {
    // the surrounding main frame
    dom.frame = document.createElement("DIV");
    dom.frame.className = "timeline-frame";
    dom.frame.style.position = "relative";
    dom.frame.style.overflow = "hidden";
    dom.container.appendChild(dom.frame);
  }

  if (options.autoHeight) {
    dom.frame.style.height = size.frameHeight + "px";
  }
  else {
    dom.frame.style.height = options.height || "100%";
  }
  dom.frame.style.width = options.width  || "100%";

  this.redrawContent();
  this.redrawGroups();
  this.redrawCurrentTime();
  this.redrawCustomTime();
  this.redrawNavigation();
}


/**
 * Redraw the content of the timeline: the axis and the items
 */
links.Timeline.prototype.redrawContent = function() {
  var dom = this.dom,
    size = this.size;

  if (!dom.content) {
    // create content box where the axis and canvas will
    dom.content = document.createElement("DIV");
    //this.frame.className = "timeline-frame";
    dom.content.style.position = "relative";
    dom.content.style.overflow = "hidden";
    dom.frame.appendChild(dom.content);

    var timelines = document.createElement("DIV");
    timelines.style.position = "absolute";
    timelines.style.left = "0px";
    timelines.style.top = "0px";
    timelines.style.height = "100%";
    timelines.style.width = "0px";
    dom.content.appendChild(timelines);
    dom.contentTimelines = timelines;

    var params = this.eventParams,
      me = this;
    if (!params.onMouseDown) {
      params.onMouseDown = function (event) {me.onMouseDown(event);};
      links.Timeline.addEventListener(dom.content, "mousedown", params.onMouseDown);
    }
    if (!params.onTouchStart) {
      params.onTouchStart = function (event) {me.onTouchStart(event);};
      links.Timeline.addEventListener(dom.content, "touchstart", params.onTouchStart);
    }
    if (!params.onMouseWheel) {
      params.onMouseWheel = function (event) {me.onMouseWheel(event);};
      links.Timeline.addEventListener(dom.content, "mousewheel", params.onMouseWheel);
    }
    if (!params.onDblClick) {
      params.onDblClick = function (event) {me.onDblClick(event);};
      links.Timeline.addEventListener(dom.content, "dblclick", params.onDblClick);
    }
  }
  dom.content.style.left = size.contentLeft + "px";
  dom.content.style.top = "0px";
  dom.content.style.width = size.contentWidth + "px";
  dom.content.style.height = size.frameHeight + "px";

  this.redrawAxis();
  this.redrawItems();
  this.redrawDeleteButton();
  this.redrawDragAreas();
}

/**
 * Redraw the timeline axis with minor and major labels
 */
links.Timeline.prototype.redrawAxis = function() {
  var dom = this.dom,
    options = this.options,
    size = this.size,
    step = this.step;

  var axis = dom.axis;
  if (!axis) {
    axis = {};
    dom.axis = axis;
  }
  if (size.axis.properties === undefined) {
    size.axis.properties = {};
  }
  if (axis.minorTexts === undefined) {
    axis.minorTexts = [];
  }
  if (axis.minorLines === undefined) {
    axis.minorLines = [];
  }
  if (axis.majorTexts === undefined) {
    axis.majorTexts = [];
  }
  if (axis.majorLines === undefined) {
    axis.majorLines = [];
  }

  if (!axis.frame) {
    axis.frame = document.createElement("DIV");
    axis.frame.style.position = "absolute";
    axis.frame.style.left = "0px";
    axis.frame.style.top = "0px";
    dom.content.appendChild(axis.frame);
  }

  // take axis offline
  dom.content.removeChild(axis.frame);

  axis.frame.style.width = (size.contentWidth) + "px";
  axis.frame.style.height = (size.axis.height) + "px";

  // the drawn axis is more wide than the actual visual part, such that
  // the axis can be dragged without having to redraw it each time again.
  var start = this.screenToTime(0);
  var end = this.screenToTime(size.contentWidth);
  var width = size.contentWidth;

  // calculate minimum step (in milliseconds) based on character size
  this.minimumStep = this.screenToTime(size.axis.characterMinorWidth * 6).valueOf() -
                     this.screenToTime(0).valueOf();

  step.setRange(start, end, this.minimumStep);

  this.redrawAxisCharacters();

  this.redrawAxisStartOverwriting();

  step.start();
  var xFirstMajorLabel = undefined;
  while (!step.end()) {
    var cur = step.getCurrent(),
        x = this.timeToScreen(cur),
        isMajor = step.isMajor();

    this.redrawAxisMinorText(x, step.getLabelMinor());

    if (isMajor && options.showMajorLabels) {
      if (x > 0) {
        if (xFirstMajorLabel === undefined) {
          xFirstMajorLabel = x;
        }
        this.redrawAxisMajorText(x, step.getLabelMajor());
      }
      this.redrawAxisMajorLine(x);
    }
    else {
      this.redrawAxisMinorLine(x);
    }

    step.next();
  }

  // create a major label on the left when needed
  if (options.showMajorLabels) {
    var leftTime = this.screenToTime(0),
      leftText = this.step.getLabelMajor(leftTime),
      width = leftText.length * size.axis.characterMajorWidth + 10;// estimation

    if (xFirstMajorLabel === undefined || width < xFirstMajorLabel) {
      this.redrawAxisMajorText(0, leftText, leftTime);
    }
  }

  this.redrawAxisHorizontal();

  // cleanup left over labels
  this.redrawAxisEndOverwriting();

  // put axis online
  dom.content.insertBefore(axis.frame, dom.content.firstChild);

}

/**
 * Create characters used to determine the size of text on the axis
 */
links.Timeline.prototype.redrawAxisCharacters = function () {
  // calculate the width and height of a single character
  // this is used to calculate the step size, and also the positioning of the
  // axis
  var dom = this.dom,
    axis = dom.axis;

  if (!axis.characterMinor) {
    var text = document.createTextNode("0");
    var characterMinor = document.createElement("DIV");
    characterMinor.className = "timeline-axis-text timeline-axis-text-minor";
    characterMinor.appendChild(text);
    characterMinor.style.position = "absolute";
    characterMinor.style.visibility = "hidden";
    characterMinor.style.paddingLeft = "0px";
    characterMinor.style.paddingRight = "0px";
    axis.frame.appendChild(characterMinor);

    axis.characterMinor = characterMinor;
  }

  if (!axis.characterMajor) {
    var text = document.createTextNode("0");
    var characterMajor = document.createElement("DIV");
    characterMajor.className = "timeline-axis-text timeline-axis-text-major";
    characterMajor.appendChild(text);
    characterMajor.style.position = "absolute";
    characterMajor.style.visibility = "hidden";
    characterMajor.style.paddingLeft = "0px";
    characterMajor.style.paddingRight = "0px";
    axis.frame.appendChild(characterMajor);

    axis.characterMajor = characterMajor;
  }
}

/**
 * Initialize redraw of the axis. All existing labels and lines will be
 * overwritten and reused.
 */
links.Timeline.prototype.redrawAxisStartOverwriting = function () {
  var properties = this.size.axis.properties;

  properties.minorTextNum = 0;
  properties.minorLineNum = 0;
  properties.majorTextNum = 0;
  properties.majorLineNum = 0;
}

/**
 * End of overwriting HTML DOM elements of the axis.
 * remaining elements will be removed
 */
links.Timeline.prototype.redrawAxisEndOverwriting = function () {
  var dom = this.dom,
    props = this.size.axis.properties,
    frame = this.dom.axis.frame;

  // remove leftovers
  var minorTexts = dom.axis.minorTexts,
      num = props.minorTextNum;
  while (minorTexts.length > num) {
    var minorText = minorTexts[num];
    frame.removeChild(minorText);
    minorTexts.splice(num, 1);
  }

  var minorLines = dom.axis.minorLines,
      num = props.minorLineNum;
  while (minorLines.length > num) {
    var minorLine = minorLines[num];
    frame.removeChild(minorLine);
    minorLines.splice(num, 1);
  }

  var majorTexts = dom.axis.majorTexts,
      num = props.majorTextNum;
  while (majorTexts.length > num) {
    var majorText = majorTexts[num];
    frame.removeChild(majorText);
    majorTexts.splice(num, 1);
  }

  var majorLines = dom.axis.majorLines,
      num = props.majorLineNum;
  while (majorLines.length > num) {
    var majorLine = majorLines[num];
    frame.removeChild(majorLine);
    majorLines.splice(num, 1);
  }
}

/**
 * Redraw the horizontal line and background of the axis
 */
links.Timeline.prototype.redrawAxisHorizontal = function() {
  var axis = this.dom.axis,
    size = this.size;

  if (!axis.backgroundLine) {
    // create the axis line background (for a background color or so)
    var backgroundLine = document.createElement("DIV");
    backgroundLine.className = "timeline-axis";
    backgroundLine.style.position = "absolute";
    backgroundLine.style.left = "0px";
    backgroundLine.style.width = "100%";
    backgroundLine.style.border = "none";
    axis.frame.insertBefore(backgroundLine, axis.frame.firstChild);

    axis.backgroundLine = backgroundLine;
  }
  axis.backgroundLine.style.top = size.axis.top + "px";
  axis.backgroundLine.style.height = size.axis.height + "px";

  if (axis.line) {
    // put this line at the end of all childs
    var line = axis.frame.removeChild(axis.line);
    axis.frame.appendChild(line);
  }
  else {
    // make the axis line
    var line = document.createElement("DIV");
    line.className = "timeline-axis";
    line.style.position = "absolute";
    line.style.left = "0px";
    line.style.width = "100%";
    line.style.height = "0px";
    axis.frame.appendChild(line);

    axis.line = line;
  }
  axis.line.style.top = size.axis.line + "px";

}

/**
 * Create a minor label for the axis at position x
 * @param {Number} x
 * @param {String} text
 */
links.Timeline.prototype.redrawAxisMinorText = function (x, text) {
  var size = this.size,
      dom = this.dom,
      props = size.axis.properties,
      frame = dom.axis.frame,
      minorTexts = dom.axis.minorTexts,
      index = props.minorTextNum,
      label;

  if (index < minorTexts.length) {
    label = minorTexts[index]
  }
  else {
    // create new label
    var content = document.createTextNode(""),
      label = document.createElement("DIV");
    label.appendChild(content);
    label.className = "timeline-axis-text timeline-axis-text-minor";
    label.style.position = "absolute";

    frame.appendChild(label);

    minorTexts.push(label);
  }

  label.childNodes[0].nodeValue = text;
  label.style.left = x + "px";
  label.style.top  = size.axis.labelMinorTop + "px";
  //label.title = title;  // TODO: this is a heavy operation

  props.minorTextNum++;
}

/**
 * Create a minor line for the axis at position x
 * @param {Number} x
 */
links.Timeline.prototype.redrawAxisMinorLine = function (x) {
  var axis = this.size.axis,
      dom = this.dom,
      props = axis.properties,
      frame = dom.axis.frame,
      minorLines = dom.axis.minorLines,
      index = props.minorLineNum,
      line;

  if (index < minorLines.length) {
    line = minorLines[index];
  }
  else {
    // create vertical line
    line = document.createElement("DIV");
    line.className = "timeline-axis-grid timeline-axis-grid-minor";
    line.style.position = "absolute";
    line.style.width = "0px";

    frame.appendChild(line);
    minorLines.push(line);
  }

  line.style.top = axis.lineMinorTop + "px";
  line.style.height = axis.lineMinorHeight + "px";
  line.style.left = (x - axis.lineMinorWidth/2) + "px";

  props.minorLineNum++;
}

/**
 * Create a Major label for the axis at position x
 * @param {Number} x
 * @param {String} text
 */
links.Timeline.prototype.redrawAxisMajorText = function (x, text) {
  var size = this.size,
      props = size.axis.properties,
      frame = this.dom.axis.frame,
      majorTexts = this.dom.axis.majorTexts,
      index = props.majorTextNum,
      label;

  if (index < majorTexts.length) {
    label = majorTexts[index];
  }
  else {
    // create label
    var content = document.createTextNode(text);
    label = document.createElement("DIV");
    label.className = "timeline-axis-text timeline-axis-text-major";
    label.appendChild(content);
    label.style.position = "absolute";
    label.style.top = "0px";

    frame.appendChild(label);
    majorTexts.push(label);
  }

  label.childNodes[0].nodeValue = text;
  label.style.top = size.axis.labelMajorTop + "px";
  label.style.left = x + "px";
  //label.title = title; // TODO: this is a heavy operation

  props.majorTextNum ++;
}

/**
 * Create a Major line for the axis at position x
 * @param {Number} x
 */
links.Timeline.prototype.redrawAxisMajorLine = function (x) {
  var size = this.size,
      props = size.axis.properties,
      axis = this.size.axis,
      frame = this.dom.axis.frame,
      majorLines = this.dom.axis.majorLines,
      index = props.majorLineNum,
      line;

  if (index < majorLines.length) {
    var line = majorLines[index];
  }
  else {
    // create vertical line
    line = document.createElement("DIV");
    line.className = "timeline-axis-grid timeline-axis-grid-major";
    line.style.position = "absolute";
    line.style.top = "0px";
    line.style.width = "0px";

    frame.appendChild(line);
    majorLines.push(line);
  }

  line.style.left = (x - axis.lineMajorWidth/2) + "px";
  line.style.height = size.frameHeight + "px";

  props.majorLineNum ++;
}

/**
 * Redraw all items
 */
links.Timeline.prototype.redrawItems = function() {
  var dom = this.dom,
    options = this.options,
    boxAlign = (options.box && options.box.align) ? options.box.align : undefined;
    size = this.size,
    contentWidth = size.contentWidth,
    items = this.items;

  if (!dom.items) {
    dom.items = {};
  }

  // draw the frame containing the items
  var frame = dom.items.frame;
  if (!frame) {
    frame = document.createElement("DIV");
    frame.style.position = "relative";
    dom.content.appendChild(frame);
    dom.items.frame = frame;
  }

  frame.style.left = "0px";
  //frame.style.width = "0px";
  frame.style.top = size.items.top + "px";
  frame.style.height = (size.frameHeight - size.axis.height) + "px";

  // initialize arrarys for storing the items
  var ranges = dom.items.ranges;
  if (!ranges) {
    ranges = [];
    dom.items.ranges = ranges;
  }
  var boxes = dom.items.boxes;
  if (!boxes) {
    boxes = [];
    dom.items.boxes = boxes;
  }
  var dots = dom.items.dots;
  if (!dots) {
    dots = [];
    dom.items.dots = dots;
  }

  // Take frame offline
  dom.content.removeChild(frame);

  if (size.dataChanged) {
    // create the items
    var rangesCreated = ranges.length,
      boxesCreated = boxes.length,
      dotsCreated = dots.length,
      rangesUsed = 0,
      boxesUsed = 0,
      dotsUsed = 0,
      itemsLength = items.length;

    for (var i = 0, iMax = items.length; i < iMax; i++) {
      var item = items[i];
      switch (item.type) {
        case 'range':
          if (rangesUsed < rangesCreated) {
            // reuse existing range
            var domItem = ranges[rangesUsed];
            domItem.firstChild.innerHTML = item.content;
            domItem.style.display = '';
            item.dom = domItem;
            rangesUsed++;
          }
          else {
            // create a new range
            var domItem = this.createEventRange(item.content);
            ranges[rangesUsed] = domItem;
            frame.appendChild(domItem);
            item.dom = domItem;
            rangesUsed++;
            rangesCreated++;
          }
          break;

        case 'box':
          if (boxesUsed < boxesCreated) {
            // reuse existing box
            var domItem = boxes[boxesUsed];
            domItem.firstChild.innerHTML = item.content;
            domItem.style.display = '';
            item.dom = domItem;
            boxesUsed++;
          }
          else {
            // create a new box
            var domItem = this.createEventBox(item.content);
            boxes[boxesUsed] = domItem;
            frame.appendChild(domItem);
            frame.insertBefore(domItem.line, frame.firstChild);
            // Note: line must be added in front of the items,
            //       such that it stays below all items
            frame.appendChild(domItem.dot);
            item.dom = domItem;
            boxesUsed++;
            boxesCreated++;
          }
          break;

        case 'dot':
          if (dotsUsed < dotsCreated) {
            // reuse existing box
            var domItem = dots[dotsUsed];
            domItem.firstChild.innerHTML = item.content;
            domItem.style.display = '';
            item.dom = domItem;
            dotsUsed++;
          }
          else {
            // create a new box
            var domItem = this.createEventDot(item.content);
            dots[dotsUsed] = domItem;
            frame.appendChild(domItem);
            item.dom = domItem;
            dotsUsed++;
            dotsCreated++;
          }
          break;

        default:
          // do nothing
          break;
      }
    }

    // remove redundant items when needed
    for (var i = rangesUsed; i < rangesCreated; i++) {
      frame.removeChild(ranges[i]);
    }
    ranges.splice(rangesUsed, rangesCreated - rangesUsed);
    for (var i = boxesUsed; i < boxesCreated; i++) {
      var box = boxes[i];
      frame.removeChild(box.line);
      frame.removeChild(box.dot);
      frame.removeChild(box);
    }
    boxes.splice(boxesUsed, boxesCreated - boxesUsed);
    for (var i = dotsUsed; i < dotsCreated; i++) {
      frame.removeChild(dots[i]);
    }
    dots.splice(dotsUsed, dotsCreated - dotsUsed);
  }

  // reposition all items
  for (var i = 0, iMax = items.length; i < iMax; i++) {
    var item = items[i],
      domItem = item.dom;

    switch (item.type) {
      case 'range':
        var left = this.timeToScreen(item.start),
          right = this.timeToScreen(item.end);

        // limit the width of the item, as browsers cannot draw very wide divs
        if (left < -contentWidth) {
          left = -contentWidth;
        }
        if (right > 2 * contentWidth) {
          right = 2 * contentWidth;
        }

        var visible = right > -contentWidth && left < 2 * contentWidth;
        if (visible || size.dataChanged) {
          // when data is changed, all items must be kept visible, as their heights must be measured
          if (item.hidden) {
            item.hidden = false;
            domItem.style.display = '';
          }
          domItem.style.top = item.top + "px";
          domItem.style.left = left + "px";
          //domItem.style.width = Math.max(right - left - 2 * item.borderWidth, 1) + "px"; // TODO: borderWidth
          domItem.style.width = Math.max(right - left, 1) + "px";
        }
        else {
          // hide when outside of the current window
          if (!item.hidden) {
            domItem.style.display = 'none';
            item.hidden = true;
          }
        }

        break;

      case 'box':
        var left = this.timeToScreen(item.start);

        var axisOnTop = options.axisOnTop,
          axisHeight = size.axis.height,
          axisTop = size.axis.top;
        var visible = ((left + item.width/2 > -contentWidth) &&
          (left - item.width/2 < 2 * contentWidth));
        if (visible || size.dataChanged) {
          // when data is changed, all items must be kept visible, as their heights must be measured
          if (item.hidden) {
            item.hidden = false;
            domItem.style.display = '';
            domItem.line.style.display = '';
            domItem.dot.style.display = '';
          }
          domItem.style.top = item.top + "px";
          if (boxAlign == 'right') {
            domItem.style.left = (left - item.width) + "px";
          }
          else if (boxAlign == 'left') {
            domItem.style.left = (left) + "px";
          }
          else { // default or 'center'
            domItem.style.left = (left - item.width/2) + "px";
          }

          var line = domItem.line;
          line.style.left = (left - item.lineWidth/2) + "px";
          if (axisOnTop) {
            //line.style.top = axisHeight + "px"; // TODO: cleanup
            //line.style.height = (item.top - axisHeight) + "px";
            line.style.top = "0px";
            line.style.height = Math.max(item.top, 0) + "px";
          }
          else {
            line.style.top = (item.top + item.height) + "px";
            line.style.height = Math.max(axisTop - item.top - item.height, 0) + "px";
          }

          var dot = domItem.dot;
          dot.style.left = (left - item.dotWidth/2) + "px";
          dot.style.top = (axisTop - item.dotHeight/2) + "px";
        }
        else {
          // hide when outside of the current window
          if (!item.hidden) {
            domItem.style.display = 'none';
            domItem.line.style.display = 'none';
            domItem.dot.style.display = 'none';
            item.hidden = true;
          }
        }
        break;

      case 'dot':
        var left = this.timeToScreen(item.start);

        var axisOnTop = options.axisOnTop,
          axisHeight = size.axis.height,
          axisTop = size.axis.top;
        var visible = (left + item.width > -contentWidth) && (left < 2 * contentWidth);
        if (visible || size.dataChanged) {
          // when data is changed, all items must be kept visible, as their heights must be measured
          if (item.hidden) {
            item.hidden = false;
            domItem.style.display = '';
          }
          domItem.style.top = item.top + "px";
          domItem.style.left = (left - item.dotWidth / 2) + "px";

          domItem.content.style.marginLeft = (1.5 * item.dotWidth) + "px";
          //domItem.content.style.marginRight = (0.5 * item.dotWidth) + "px"; // TODO
          domItem.dot.style.top = ((item.height - item.dotHeight) / 2) + "px";
        }
        else {
          // hide when outside of the current window
          if (!item.hidden) {
            domItem.style.display = 'none';
            item.hidden = true;
          }
        }
        break;

      default:
        // do nothing
        break;
    }
  }

  // move selected item to the end, to ensure that it is always on top
  if (this.selection) {
    var item = this.selection.item;
    frame.removeChild(item);
    frame.appendChild(item);
  }

  // put frame online again
  dom.content.appendChild(frame);

  /* TODO
  // retrieve all image sources from the items, and set a callback once
  // all images are retrieved
  var urls = [];
  var timeline = this;
  links.Timeline.filterImageUrls(frame, urls);
  if (urls.length) {
    for (var i = 0; i < urls.length; i++) {
      var url = urls[i];
      var callback = function (url) {
        timeline.redraw();
      };
      var sendCallbackWhenAlreadyLoaded = false;
      links.imageloader.load(url, callback, sendCallbackWhenAlreadyLoaded);
    }
  }
  */
}


/**
 * Create an event in the timeline, with (optional) formatting: inside a box
 * with rounded corners, and a vertical line+dot to the axis.
 * @param {string} content    The content for the event. This can be plain text
 *                            or HTML code.
 */
links.Timeline.prototype.createEventBox = function(content) {
  // background box
  var divBox = document.createElement("DIV");
  divBox.style.position = "absolute";
  divBox.style.left  = "0px";
  divBox.style.top = "0px";
  divBox.className  = "timeline-event timeline-event-box";

  // contents box (inside the background box). used for making margins
  var divContent = document.createElement("DIV");
  divContent.className = "timeline-event-content";
  divContent.innerHTML = content;
  divBox.appendChild(divContent);

  // line to axis
  var divLine = document.createElement("DIV");
  divLine.style.position = "absolute";
  divLine.style.width = "0px";
  divLine.className = "timeline-event timeline-event-line";
  // important: the vertical line is added at the front of the list of elements,
  // so it will be drawn behind all boxes and ranges
  divBox.line = divLine;

  // dot on axis
  var divDot = document.createElement("DIV");
  divDot.style.position = "absolute";
  divDot.style.width  = "0px";
  divDot.style.height = "0px";
  divDot.className  = "timeline-event timeline-event-dot";
  divBox.dot = divDot;

  return divBox;
}


/**
 * Create an event in the timeline: a dot, followed by the content.
 * @param {string} content    The content for the event. This can be plain text
 *                            or HTML code.
 */
links.Timeline.prototype.createEventDot = function(content) {
  // background box
  var divBox = document.createElement("DIV");
  divBox.style.position = "absolute";

  // contents box, right from the dot
  var divContent = document.createElement("DIV");
  divContent.className = "timeline-event-content";
  divContent.innerHTML = content;
  divBox.appendChild(divContent);

  // dot at start
  var divDot = document.createElement("DIV");
  divDot.style.position = "absolute";
  divDot.className = "timeline-event timeline-event-dot";
  divDot.style.width = "0px";
  divDot.style.height = "0px";
  divBox.appendChild(divDot);

  divBox.content = divContent;
  divBox.dot = divDot;

  return divBox;
}


/**
 * Create an event range as a beam in the timeline.
 * @param {string}  content    The content for the event. This can be plain text
 *                             or HTML code.
 */
links.Timeline.prototype.createEventRange = function(content) {
  // background box
  var divBox = document.createElement("DIV");
  divBox.style.position = "absolute";
  divBox.className = "timeline-event timeline-event-range";

  // contents box
  var divContent = document.createElement("DIV");
  divContent.className = "timeline-event-content";
  divContent.innerHTML = content;
  divBox.appendChild(divContent);

  return divBox;
}

/**
 * Redraw the group labels
 */
links.Timeline.prototype.redrawGroups = function() {
  var dom = this.dom,
    options = this.options,
    size = this.size,
    groups = this.groups;

  if (dom.groups === undefined) {
    dom.groups = {};
  }

  var labels = dom.groups.labels;
  if (!labels) {
    labels = [];
    dom.groups.labels = labels;
  }
  var labelLines = dom.groups.labelLines;
  if (!labelLines) {
    labelLines = [];
    dom.groups.labelLines = labelLines;
  }
  var itemLines = dom.groups.itemLines;
  if (!itemLines) {
    itemLines = [];
    dom.groups.itemLines = itemLines;
  }

  // create the frame for holding the groups
  var frame = dom.groups.frame;
  if (!frame) {
    var frame =  document.createElement("DIV");
    frame.className = "timeline-groups-axis";
    frame.style.position = "absolute";
    frame.style.overflow = "hidden";
    frame.style.top = "0px";
    frame.style.height = "100%";

    dom.frame.appendChild(frame);
    dom.groups.frame = frame;
  }

  frame.style.left = size.groupsLeft + "px";
  frame.style.width = (options.groupsWidth !== undefined) ?
    options.groupsWidth :
    size.groupsWidth + "px";

  // hide groups axis when there are no groups
  if (groups.length == 0) {
    frame.style.display = 'none';
  }
  else {
    frame.style.display = '';
  }

  if (size.dataChanged) {
    // create the items
    var current = labels.length,
      needed = groups.length;

    // overwrite existing items
    for (var i = 0, iMax = Math.min(current, needed); i < iMax; i++) {
      var group = groups[i];
      var label = labels[i];
      label.innerHTML = group.content;
      label.style.display = '';
    }

    // append new items when needed
    for (var i = current; i < needed; i++) {
      var group = groups[i];

      // create text label
      var label = document.createElement("DIV");
      label.className = "timeline-groups-text";
      label.style.position = "absolute";
      if (options.groupsWidth === undefined) {
        label.style.whiteSpace = "nowrap";
      }
      label.innerHTML = group.content;
      frame.appendChild(label);
      labels[i] = label;

      // create the grid line between the group labels
      var labelLine = document.createElement("DIV");
      labelLine.className = "timeline-axis-grid timeline-axis-grid-minor";
      labelLine.style.position = "absolute";
      labelLine.style.left = "0px";
      labelLine.style.width = "100%";
      labelLine.style.height = "0px";
      labelLine.style.borderTopStyle = "solid";
      frame.appendChild(labelLine);
      labelLines[i] = labelLine;

      // create the grid line between the items
      var itemLine = document.createElement("DIV");
      itemLine.className = "timeline-axis-grid timeline-axis-grid-minor";
      itemLine.style.position = "absolute";
      itemLine.style.left = "0px";
      itemLine.style.width = "100%";
      itemLine.style.height = "0px";
      itemLine.style.borderTopStyle = "solid";
      dom.content.insertBefore(itemLine, dom.content.firstChild);
      itemLines[i] = itemLine;
    }

    // remove redundant items from the DOM when needed
    for (var i = needed; i < current; i++) {
      var label = labels[i],
        labelLine = labelLines[i],
        itemLine = itemLines[i];

      frame.removeChild(label);
      frame.removeChild(labelLine);
      dom.content.removeChild(itemLine);
    }
    labels.splice(needed, current - needed);
    labelLines.splice(needed, current - needed);
    itemLines.splice(needed, current - needed);

    frame.style.borderStyle = options.groupsOnRight ?
      "none none none solid" :
      "none solid none none";
  }

  // position the groups
  for (var i = 0, iMax = groups.length; i < iMax; i++) {
    var group = groups[i],
      label = labels[i],
      labelLine = labelLines[i],
      itemLine = itemLines[i];

    label.style.top = group.labelTop + "px";
    labelLine.style.top = group.lineTop + "px";
    itemLine.style.top = group.lineTop + "px";
    itemLine.style.width = size.contentWidth + "px";
  }

  if (!dom.groups.background) {
    // create the axis grid line background
    var background = document.createElement("DIV");
    background.className = "timeline-axis";
    background.style.position = "absolute";
    background.style.left = "0px";
    background.style.width = "100%";
    background.style.border = "none";

    frame.appendChild(background);
    dom.groups.background = background;
  }
  dom.groups.background.style.top = size.axis.top + 'px';
  dom.groups.background.style.height = size.axis.height + 'px';

  if (!dom.groups.line) {
    // create the axis grid line
    var line = document.createElement("DIV");
    line.className = "timeline-axis";
    line.style.position = "absolute";
    line.style.left = "0px";
    line.style.width = "100%";
    line.style.height = "0px";

    frame.appendChild(line);
    dom.groups.line = line;
  }
  dom.groups.line.style.top = size.axis.line + 'px';
}


/**
 * Redraw the current time bar
 */
links.Timeline.prototype.redrawCurrentTime = function() {
  var options = this.options,
    dom = this.dom,
    size = this.size;

  if (!options.showCurrentTime) {
    if (dom.currentTime) {
      dom.contentTimelines.removeChild(dom.currentTime);
      delete dom.currentTime;
    }

    return;
  }

  if (!dom.currentTime) {
    // create the current time bar
    var currentTime = document.createElement("DIV");
    currentTime.className = "timeline-currenttime";
    currentTime.style.position = "absolute";
    currentTime.style.top = "0px";
    currentTime.style.height = "100%";

    dom.contentTimelines.appendChild(currentTime);
    dom.currentTime = currentTime;
  }

  var now = new Date();
  var nowOffset = new Date(now.getTime() + this.clientTimeOffset);
  var x = this.timeToScreen(nowOffset);

  var visible = (x > -size.contentWidth && x < 2 * size.contentWidth);
  dom.currentTime.style.display = visible ? '' : 'none';
  dom.currentTime.style.left = x + "px";
  dom.currentTime.title = "Current time: " + nowOffset;

  // start a timer to adjust for the new time
  if (this.currentTimeTimer != undefined) {
    clearTimeout(this.currentTimeTimer);
    delete this.currentTimeTimer;
  }
  var timeline = this;
  var onTimeout = function() {
    timeline.redrawCurrentTime();
  }
  // the time equal to the width of one pixel, divided by 2 for more smoothness
  var interval = 1 / this.conversion.factor / 2;
  if (interval < 30) interval = 30;
  this.currentTimeTimer = setTimeout(onTimeout, interval);
}

/**
 * Redraw the custom time bar
 */
links.Timeline.prototype.redrawCustomTime = function() {
  var options = this.options,
    dom = this.dom,
    size = this.size;

  if (!options.showCustomTime) {
    if (dom.customTime) {
      dom.contentTimelines.removeChild(dom.customTime);
      delete dom.customTime;
    }

    return;
  }

  if (!dom.customTime) {
    var customTime = document.createElement("DIV");
    customTime.className = "timeline-customtime";
    customTime.style.position = "absolute";
    customTime.style.top = "0px";
    customTime.style.height = "100%";

    var drag = document.createElement("DIV");
    drag.style.position = "relative";
    drag.style.top = "0px";
    drag.style.left = "-10px";
    drag.style.height = "100%";
    drag.style.width = "20px";
    customTime.appendChild(drag);

    dom.contentTimelines.appendChild(customTime);
    dom.customTime = customTime;

    // initialize parameter
    this.customTime = new Date();
  }

  var x = this.timeToScreen(this.customTime),
    visible = (x > -size.contentWidth && x < 2 * size.contentWidth);
  dom.customTime.style.display = visible ? '' : 'none';
  dom.customTime.style.left = x + "px";
  dom.customTime.title = "Time: " + this.customTime;
}


/**
 * Redraw the delete button, on the top right of the currently selected item
 * if there is no item selected, the button is hidden.
 */
links.Timeline.prototype.redrawDeleteButton = function () {
  var timeline = this,
    options = this.options,
    dom = this.dom,
    size = this.size,
    frame = dom.items.frame;

  if (!options.editable) {
    return;
  }

  var deleteButton = dom.items.deleteButton;
  if (!deleteButton) {
    // create a delete button
    deleteButton = document.createElement("DIV");
    deleteButton.className = "timeline-navigation-delete";
    deleteButton.style.position = "absolute";

    frame.appendChild(deleteButton);
    dom.items.deleteButton = deleteButton;
  }

  if (this.selection) {
    var index = this.selection.index,
      item = this.items[index],
      domItem = this.selection.item,
      right,
      top = item.top;

    switch (item.type) {
      case 'range':
        right = this.timeToScreen(item.end);
        break;

      case 'box':
        //right = this.timeToScreen(item.start) + item.width / 2 + item.borderWidth; // TODO: borderWidth
        right = this.timeToScreen(item.start) + item.width / 2;
        break;

      case 'dot':
        right = this.timeToScreen(item.start) + item.width;
        break;
    }

    // limit the position
    if (right < -size.contentWidth) {
      right = -size.contentWidth;
    }
    if (right > 2 * size.contentWidth) {
      right = 2 * size.contentWidth;
    }

    deleteButton.style.left = right + 'px';
    deleteButton.style.top = top + 'px';
    deleteButton.style.display = '';
    frame.removeChild(deleteButton);
    frame.appendChild(deleteButton);
  }
  else {
    deleteButton.style.display = 'none';
  }
}


/**
 * Redraw the drag areas. When an item (ranges only) is selected,
 * it gets a drag area on the left and right side, to change its width
 */
links.Timeline.prototype.redrawDragAreas = function () {
  var timeline = this,
    options = this.options,
    dom = this.dom,
    size = this.size,
    frame = this.dom.items.frame;

  if (!options.editable) {
    return;
  }

  // create left drag area
  var dragLeft = dom.items.dragLeft;
  if (!dragLeft) {
    dragLeft = document.createElement("DIV");
    dragLeft.style.width = options.dragAreaWidth + "px";
    dragLeft.style.position = "absolute";
    dragLeft.style.cursor = "w-resize";

    frame.appendChild(dragLeft);
    dom.items.dragLeft = dragLeft;
  }

  // create right drag area
  var dragRight = dom.items.dragRight;
  if (!dragRight) {
    dragRight = document.createElement("DIV");
    dragRight.style.width = options.dragAreaWidth + "px";
    dragRight.style.position = "absolute";
    dragRight.style.cursor = "e-resize";

    frame.appendChild(dragRight);
    dom.items.dragRight = dragRight;
  }

  // reposition left and right drag area
  if (this.selection) {
    var index = this.selection.index,
      item = this.items[index];

    if (item.type == 'range') {
      var domItem = item.dom,
      left = this.timeToScreen(item.start),
      right = this.timeToScreen(item.end),
      top = item.top,
      height = item.height;

      dragLeft.style.left = left + 'px';
      dragLeft.style.top = top + 'px';
      dragLeft.style.height = height + 'px';
      dragLeft.style.display = '';
      frame.removeChild(dragLeft);
      frame.appendChild(dragLeft);

      dragRight.style.left = (right - options.dragAreaWidth) + 'px';
      dragRight.style.top = top + 'px';
      dragRight.style.height = height + 'px';
      dragRight.style.display = '';
      frame.removeChild(dragRight);
      frame.appendChild(dragRight);
    }
  }
  else {
    dragLeft.style.display = 'none';
    dragRight.style.display = 'none';
  }
}



/**
 * Create the navigation buttons for zooming and moving
 */
links.Timeline.prototype.redrawNavigation = function () {
  var timeline = this,
    options = this.options,
    dom = this.dom,
    frame = dom.frame,
    navBar = dom.navBar;

  if (!navBar) {
    if (options.editable || options.showNavigation) {
      // create a navigation bar containing the navigation buttons
      navBar = document.createElement("DIV");
      navBar.style.position = "absolute";
      navBar.className = "timeline-navigation";
      if (options.groupsOnRight) {
        navBar.style.left = '10px';
      }
      else {
        navBar.style.right = '10px';
      }
      if (options.axisOnTop) {
        navBar.style.bottom = '10px';
      }
      else {
        navBar.style.top = '10px';
      }
      dom.navBar = navBar;
      frame.appendChild(navBar);
    }

    if (options.editable && options.showButtonAdd) {
      // create a new in button
      navBar.addButton = document.createElement("DIV");
      navBar.addButton.className = "timeline-navigation-new";

      navBar.addButton.title = "Create new event";
      var onAdd = function(event) {
        links.Timeline.preventDefault(event);
        links.Timeline.stopPropagation(event);

        // create a new event at the center of the frame
        var w = timeline.size.contentWidth;
        var x = w / 2;
        var xstart = timeline.screenToTime(x - w / 10); // subtract 10% of timeline width
        var xend = timeline.screenToTime(x + w / 10); // add 10% of timeline width
        if (options.snapEvents) {
          timeline.step.snap(xstart);
          timeline.step.snap(xend);
        }

        var content = "New";
        var group = timeline.groups.length ? timeline.groups[0].content : undefined;

        timeline.addItem({
          'start': xstart,
          'end': xend,
          'content': content,
          'group': group
        });
        var index = (timeline.items.length - 1);
        timeline.selectItem(index);

        timeline.applyAdd = true;

        // fire an add event.
        // Note that the change can be canceled from within an event listener if
        // this listener calls the method cancelAdd().
        timeline.trigger('add');

        if (!timeline.applyAdd) {
          // undo an add
          timeline.deleteItem(index);
        }
        timeline.redrawDeleteButton();
        timeline.redrawDragAreas();
      };
      links.Timeline.addEventListener(navBar.addButton, "mousedown", onAdd);
      navBar.appendChild(navBar.addButton);
    }

    if (options.editable && options.showButtonAdd && options.showNavigation) {
      // create a separator line
      navBar.addButton.style.borderRightWidth = "1px";
      navBar.addButton.style.borderRightStyle = "solid";
    }

    if (options.showNavigation) {
      // create a zoom in button
      navBar.zoomInButton = document.createElement("DIV");
      navBar.zoomInButton.className = "timeline-navigation-zoom-in";
      navBar.zoomInButton.title = "Zoom in";
      var onZoomIn = function(event) {
        links.Timeline.preventDefault(event);
        links.Timeline.stopPropagation(event);
        timeline.zoom(0.4);
        timeline.trigger("rangechange");
        timeline.trigger("rangechanged");
      };
      links.Timeline.addEventListener(navBar.zoomInButton, "mousedown", onZoomIn);
      navBar.appendChild(navBar.zoomInButton);

      // create a zoom out button
      navBar.zoomOutButton = document.createElement("DIV");
      navBar.zoomOutButton.className = "timeline-navigation-zoom-out";
      navBar.zoomOutButton.title = "Zoom out";
      var onZoomOut = function(event) {
        links.Timeline.preventDefault(event);
        links.Timeline.stopPropagation(event);
        timeline.zoom(-0.4);
        timeline.trigger("rangechange");
        timeline.trigger("rangechanged");
      };
      links.Timeline.addEventListener(navBar.zoomOutButton, "mousedown", onZoomOut);
      navBar.appendChild(navBar.zoomOutButton);

      // create a move left button
      navBar.moveLeftButton = document.createElement("DIV");
      navBar.moveLeftButton.className = "timeline-navigation-move-left";
      navBar.moveLeftButton.title = "Move left";
      var onMoveLeft = function(event) {
        links.Timeline.preventDefault(event);
        links.Timeline.stopPropagation(event);
        timeline.move(-0.2);
        timeline.trigger("rangechange");
        timeline.trigger("rangechanged");
      };
      links.Timeline.addEventListener(navBar.moveLeftButton, "mousedown", onMoveLeft);
      navBar.appendChild(navBar.moveLeftButton);

      // create a move right button
      navBar.moveRightButton = document.createElement("DIV");
      navBar.moveRightButton.className = "timeline-navigation-move-right";
      navBar.moveRightButton.title = "Move right";
      var onMoveRight = function(event) {
        links.Timeline.preventDefault(event);
        links.Timeline.stopPropagation(event);
        timeline.move(0.2);
        timeline.trigger("rangechange");
        timeline.trigger("rangechanged");
      };
      links.Timeline.addEventListener(navBar.moveRightButton, "mousedown", onMoveRight);
      navBar.appendChild(navBar.moveRightButton);
    }
  }
}


/**
 * Set current time. This function can be used to set the time in the client
 * timeline equal with the time on a server.
 * @param {Date} time
 */
links.Timeline.prototype.setCurrentTime = function(time) {
  var now = new Date();
  this.clientTimeOffset = time.getTime() - now.getTime();

  this.redrawCurrentTime();
}

/**
 * Get current time. The time can have an offset from the real time, when
 * the current time has been changed via the method setCurrentTime.
 * @return {Date} time
 */
links.Timeline.prototype.getCurrentTime = function() {
  var now = new Date();
  return new Date(now.getTime() + this.clientTimeOffset);
}


/**
 * Set custom time.
 * The custom time bar can be used to display events in past or future.
 * @param {Date} time
 */
links.Timeline.prototype.setCustomTime = function(time) {
  this.customTime = new Date(time);
  this.redrawCustomTime();
}

/**
 * Retrieve the current custom time.
 * @return {Date} customTime
 */
links.Timeline.prototype.getCustomTime = function() {
  return new Date(this.customTime);
}

/**
 * Set a custom scale. Autoscaling will be disabled.
 * For example setScale(SCALE.MINUTES, 5) will result
 * in minor steps of 5 minutes, and major steps of an hour.
 *
 * @param {Step.SCALE} newScale  A scale. Choose from SCALE.MILLISECOND,
 *                               SCALE.SECOND, SCALE.MINUTE, SCALE.HOUR,
 *                               SCALE.DAY, SCALE.MONTH, SCALE.YEAR.
 * @param {int}        newStep   A step size, by default 1. Choose for
 *                               example 1, 2, 5, or 10.
 */
links.Timeline.prototype.setScale = function(scale, step) {
  this.step.setScale(scale, step);
  this.redrawFrame();
}

/**
 * Enable or disable autoscaling
 * @param {boolean} enable  If true or not defined, autoscaling is enabled.
 *                          If false, autoscaling is disabled.
 */
links.Timeline.prototype.setAutoScale = function(enable) {
  this.step.setAutoScale(enable);
  this.redrawFrame();
}

/**
 * Redraw the timeline
 * Reloads the (linked) data table and redraws the timeline when resized.
 * See also the method checkResize
 */
links.Timeline.prototype.redraw = function() {
  this.setData(this.data);
}


/**
 * Check if the timeline is resized, and if so, redraw the timeline.
 * Useful when the webpage is resized.
 */
links.Timeline.prototype.checkResize = function() {
  var resized = this.recalcSize();
  if (resized) {
    this.redrawFrame();
  }
}

/**
 * Recursively retrieve all image urls from the images located inside a given
 * HTML element
 * @param {HTMLElement} elem
 * @param {Array with String} urls   Urls will be added here (no duplicates)
 */
links.Timeline.filterImageUrls = function(elem, urls) {
  var child = elem.firstChild;
  while (child) {
    if (child.tagName == 'IMG') {
      var url = child.src;
      if (urls.indexOf(url) == -1) {
        urls.push(url);
      }
    }

    links.Timeline.filterImageUrls(child, urls);

    child = child.nextSibling;
  }
}

/**
 * Recalculate the sizes of all frames, groups, items, axis
 * After recalcSize() is executed, the Timeline should be redrawn normally
 *
 * @return {boolean} resized   Returns true when the timeline has been resized
 */
links.Timeline.prototype.recalcSize = function() {
  var resized = false;

  var timeline = this;
    size = this.size,
    options = this.options,
    axisOnTop = options.axisOnTop,
    dom = this.dom,
    axis = dom.axis,
    groups = this.groups,
    labels = dom.groups.labels,
    items = this.items

    groupsWidth = size.groupsWidth,
    characterMinorWidth  = axis.characterMinor ? axis.characterMinor.clientWidth : 0,
    characterMinorHeight = axis.characterMinor ? axis.characterMinor.clientHeight : 0,
    characterMajorWidth  = axis.characterMajor ? axis.characterMajor.clientWidth : 0,
    characterMajorHeight = axis.characterMajor ? axis.characterMajor.clientHeight : 0,
    axisHeight = characterMinorHeight + (options.showMajorLabels ? characterMajorHeight : 0),
    actualHeight = size.actualHeight || axisHeight;

  // TODO: move checking for loaded items when creating the dom
  if (size.dataChanged) {
    // retrieve all image sources from the items, and set a callback once
    // all images are retrieved
    var urls = [];
    for (var i = 0, iMax = items.length; i < iMax; i++) {
      var item = items[i],
        domItem = item.dom;

      if (domItem) {
        links.Timeline.filterImageUrls(domItem, urls);
      }
    }
    if (urls.length) {
      for (var i = 0; i < urls.length; i++) {
        var url = urls[i];
        var callback = function (url) {
          timeline.redraw();
        };
        var sendCallbackWhenAlreadyLoaded = false;
        links.imageloader.load(url, callback, sendCallbackWhenAlreadyLoaded);
      }
    }
  }

  // check sizes of the items and groups (width and height) when the data is changed
  if (size.dataChanged) { // TODO: always calculate the size of an item?
  //if (true) {
    groupsWidth = 0;

    // loop through all groups to get the maximum width and the heights
    for (var i = 0, iMax = labels.length; i < iMax; i++) {
      var group = groups[i];
      group.width = labels[i].clientWidth;
      group.height = labels[i].clientHeight;
      group.labelHeight = group.height;

      groupsWidth = Math.max(groupsWidth, group.width);
    }

    // loop through the width and height of all items
    for (var i = 0, iMax = items.length; i < iMax; i++) {
      var item = items[i],
        domItem = item.dom,
        group = item.group;

      var width = domItem ? domItem.clientWidth : 0;
      var height = domItem ? domItem.clientHeight : 0;
      resized = resized || (item.width != width);
      resized = resized || (item.height != height);
      item.width = width;
      item.height = height;
      //item.borderWidth = (domItem.offsetWidth - domItem.clientWidth - 2) / 2; // TODO: borderWidth

      switch (item.type) {
        case 'range':
          break;

        case 'box':
          item.dotHeight = domItem.dot.offsetHeight;
          item.dotWidth = domItem.dot.offsetWidth;
          item.lineWidth = domItem.line.offsetWidth;
          break;

        case 'dot':
          item.dotHeight = domItem.dot.offsetHeight;
          item.dotWidth = domItem.dot.offsetWidth;
          item.contentHeight = domItem.content.offsetHeight;
          break;
      }

      if (group) {
        group.height = group.height ? Math.max(group.height, item.height) : item.height;
      }
    }

    // calculate the actual height of the timeline (needed for auto sizing
    // the timeline)
    actualHeight = axisHeight + 2 * options.eventMarginAxis;
    for (var i = 0, iMax = groups.length; i < iMax; i++) {
      actualHeight += groups[i].height + options.eventMargin;
    }
  }

  // calculate actual height of the timeline when there are no groups
  // but stacked items
  if (groups.length == 0 && options.autoHeight) {
    var min = 0,
      max = 0;

    if (this.animation && this.animation.finalItems) {
      // adjust the offset of all finalItems when the actualHeight has been changed
      var finalItems = this.animation.finalItems,
        finalItem = finalItems[0];
      if (finalItem && finalItem.top) {
        min = finalItem.top,
        max = finalItem.top + finalItem.height;
      }
      for (var i = 1, iMax = finalItems.length; i < iMax; i++) {
        finalItem = finalItems[i];
        min = Math.min(min, finalItem.top);
        max = Math.max(max, finalItem.top + finalItem.height);
      }
    }
    else {
      var item = items[0];
      if (item && item.top) {
        min = item.top,
        max = item.top + item.height;
      }
      for (var i = 1, iMax = items.length; i < iMax; i++) {
        var item = items[i];
        if (item.top) {
          min = Math.min(min, item.top);
          max = Math.max(max, (item.top + item.height));
        }
      }
    }

    actualHeight = (max - min) + 2 * options.eventMarginAxis + axisHeight;

    if (size.actualHeight != actualHeight && options.autoHeight && !options.axisOnTop) {
      // adjust the offset of all items when the actualHeight has been changed
      var diff = actualHeight - size.actualHeight;
      if (this.animation && this.animation.finalItems) {
        var finalItems = this.animation.finalItems;
        for (var i = 0, iMax = finalItems.length; i < iMax; i++) {
          finalItems[i].top += diff;
          finalItems[i].item.top += diff; // TODO
        }
      }
      else {
        for (var i = 0, iMax = items.length; i < iMax; i++) {
          items[i].top += diff;
        }
      }
    }
  }

  // now the heights of the elements are known, we can calculate the the
  // width and height of frame and axis and content
  // Note: IE7 has issues with giving frame.clientWidth, therefore I use offsetWidth instead
  var frameWidth  = dom.frame ? dom.frame.offsetWidth : 0,
    frameHeight = Math.max(options.autoHeight ?
      actualHeight : (dom.frame ? dom.frame.clientHeight : 0),
      options.minHeight),
    axisTop  = axisOnTop ? 0 : frameHeight - axisHeight,
    axisLine = axisOnTop ? axisHeight : axisTop,
    itemsTop = axisOnTop ? axisHeight : 0,
    contentHeight = Math.max(frameHeight - axisHeight, 0);

  if (options.groupsWidth !== undefined) {
    groupsWidth = dom.groups.frame ? dom.groups.frame.clientWidth : 0;
  }
  var groupsLeft = options.groupsOnRight ? frameWidth - groupsWidth : 0;

  if (size.dataChanged) {
    // calculate top positions of the group labels and lines
    var eventMargin = options.eventMargin,
      top = axisOnTop ?
        options.eventMarginAxis + eventMargin/2 :
        contentHeight - options.eventMarginAxis + eventMargin/2;

    for (var i = 0, iMax = groups.length; i < iMax; i++) {
      var group = groups[i];
      if (axisOnTop) {
        group.top = top;
        group.labelTop = top + axisHeight + (group.height - group.labelHeight) / 2;
        group.lineTop = top + axisHeight + group.height + eventMargin/2;
        top += group.height + eventMargin;
      }
      else {
        top -= group.height + eventMargin;
        group.top = top;
        group.labelTop = top + (group.height - group.labelHeight) / 2;
        group.lineTop = top - eventMargin/2;
      }
    }

    // calculate top position of the items
    for (var i = 0, iMax = items.length; i < iMax; i++) {
      var item = items[i],
        group = item.group;

      if (group) {
        item.top = group.top;
      }
    }

    resized = true;
  }

  resized = resized || (size.groupsWidth !== groupsWidth);
  resized = resized || (size.groupsLeft !== groupsLeft);
  resized = resized || (size.actualHeight !== actualHeight);
  size.groupsWidth = groupsWidth;
  size.groupsLeft = groupsLeft;
  size.actualHeight = actualHeight;

  resized = resized || (size.frameWidth !== frameWidth);
  resized = resized || (size.frameHeight !== frameHeight);
  size.frameWidth = frameWidth;
  size.frameHeight = frameHeight;

  resized = resized || (size.groupsWidth !== groupsWidth);
  size.groupsWidth = groupsWidth;
  size.contentLeft = options.groupsOnRight ? 0 : groupsWidth;
  size.contentWidth = Math.max(frameWidth - groupsWidth, 0);
  size.contentHeight = contentHeight;

  resized = resized || (size.axis.top !== axisTop);
  resized = resized || (size.axis.line !== axisLine);
  resized = resized || (size.axis.height !== axisHeight);
  resized = resized || (size.items.top !== itemsTop);
  size.axis.top = axisTop;
  size.axis.line = axisLine;
  size.axis.height = axisHeight;
  size.axis.labelMajorTop = options.axisOnTop ? 0 : axisLine + characterMinorHeight;
  size.axis.labelMinorTop = options.axisOnTop ?
    (options.showMajorLabels ? characterMajorHeight : 0) :
    axisLine;
  size.axis.lineMinorTop = options.axisOnTop ? size.axis.labelMinorTop : 0;
  size.axis.lineMinorHeight = options.showMajorLabels ?
    frameHeight - characterMajorHeight:
    frameHeight;
  size.axis.lineMinorWidth = dom.axis.minorLines.length ?
    dom.axis.minorLines[0].offsetWidth : 1;
  size.axis.lineMajorWidth = dom.axis.majorLines.length ?
    dom.axis.majorLines[0].offsetWidth : 1;

  size.items.top = itemsTop;

  resized = resized || (size.axis.characterMinorWidth  !== characterMinorWidth);
  resized = resized || (size.axis.characterMinorHeight !== characterMinorHeight);
  resized = resized || (size.axis.characterMajorWidth  !== characterMajorWidth);
  resized = resized || (size.axis.characterMajorHeight !== characterMajorHeight);
  size.axis.characterMinorWidth  = characterMinorWidth;
  size.axis.characterMinorHeight = characterMinorHeight;
  size.axis.characterMajorWidth  = characterMajorWidth;
  size.axis.characterMajorHeight = characterMajorHeight;

  // conversion factors can be changed when width of the Timeline is changed,
  // and when start or end are changed
  this.recalcConversion();

  return resized;
}



/**
 * Calculate the factor and offset to convert a position on screen to the
 * corresponding date and vice versa.
 * After the method calcConversionFactor is executed once, the methods screenToTime and
 * timeToScreen can be used.
 */
links.Timeline.prototype.recalcConversion = function() {
  this.conversion.offset = parseFloat(this.start.valueOf());
  this.conversion.factor = parseFloat(this.size.contentWidth) /
    parseFloat(this.end.valueOf() - this.start.valueOf());
}


/**
 * Convert a position on screen (pixels) to a datetime
 * Before this method can be used, the method calcConversionFactor must be
 * executed once.
 * @param {int}     x    Position on the screen in pixels
 * @return {Date}   time The datetime the corresponds with given position x
 */
links.Timeline.prototype.screenToTime = function(x) {
  var conversion = this.conversion,
    time = new Date(parseFloat(x) / conversion.factor + conversion.offset);
  return time;
}

/**
 * Convert a datetime (Date object) into a position on the screen
 * Before this method can be used, the method calcConversionFactor must be
 * executed once.
 * @param {Date}   time A date
 * @return {int}   x    The position on the screen in pixels which corresponds
 *                      with the given date.
 */
links.Timeline.prototype.timeToScreen = function(time) {
  var conversion = this.conversion;
  var x = (time.valueOf() - conversion.offset) * conversion.factor;
  return x;
}



/**
 * Event handler for touchstart event on mobile devices
 */
links.Timeline.prototype.onTouchStart = function(event) {
  var params = this.eventParams,
    dom = this.dom,
    me = this;

  if (params.touchDown) {
    // if already moving, return
    return;
  }

  params.touchDown = true;
  params.zoomed = false;

  this.onMouseDown(event);

  if (!params.onTouchMove) {
    params.onTouchMove = function (event) {me.onTouchMove(event);};
    links.Timeline.addEventListener(document, "touchmove", params.onTouchMove);
  }
  if (!params.onTouchEnd) {
    params.onTouchEnd  = function (event) {me.onTouchEnd(event);};
    links.Timeline.addEventListener(document, "touchend",  params.onTouchEnd);
  }
};

/**
 * Event handler for touchmove event on mobile devices
 */
links.Timeline.prototype.onTouchMove = function(event) {
  var params = this.eventParams;

  if (event.scale && event.scale !== 1) {
    params.zoomed = true;
  }

  if (!params.zoomed) {
    // move
    this.onMouseMove(event);
  }
  else {
    if (this.options.zoomable) {
      // pinch
      // TODO: pinch only supported on iPhone/iPad. Create something manually for Android?
      params.zoomed = true;

      var scale = event.scale,
        oldWidth = (params.end.valueOf() - params.start.valueOf()),
        newWidth = oldWidth / scale,
        diff = newWidth - oldWidth,
        start = new Date(parseInt(params.start.valueOf() - diff/2)),
        end = new Date(parseInt(params.end.valueOf() + diff/2));

      // TODO: determine zoom-around-date from touch positions?

      this.setVisibleChartRange(start, end);
      timeline.trigger("rangechange");

      links.Timeline.preventDefault(event);
    }
  }
};

/**
 * Event handler for touchend event on mobile devices
 */
links.Timeline.prototype.onTouchEnd = function(event) {
  var params = this.eventParams;
  params.touchDown = false;

  /* TODO: cleanup
  document.getElementById("info").innerHTML = "touchEnd";
  */

  if (params.zoomed) {
    timeline.trigger("rangechanged");
  }

  if (params.onTouchMove) {
    links.Timeline.removeEventListener(document, "touchmove", params.onTouchMove);
    delete params.onTouchMove;

  }
  if (params.onTouchEnd) {
    links.Timeline.removeEventListener(document, "touchend",  params.onTouchEnd);
    delete params.onTouchEnd;
  }

  this.onMouseUp(event);
};


/**
 * Start a moving operation inside the provided parent element
 * @param {event} event       The event that occurred (required for
 *                             retrieving the  mouse position)
 */
links.Timeline.prototype.onMouseDown = function(event) {
  event = event || window.event;

  var params = this.eventParams,
    options = this.options,
    dom = this.dom;

  // only react on left mouse button down
  var leftButtonDown = event.which ? (event.which == 1) : (event.button == 1);
  if (!leftButtonDown && !params.touchDown) {
    return;
  }

  // check if frame is not resized (causing a mismatch with the end Date)
  this.recalcSize();

  // get mouse position
  if (!params.touchDown) {
    params.mouseX = event.clientX;
    params.mouseY = event.clientY;
  }
  else {
    params.mouseX = event.targetTouches[0].clientX;
    params.mouseY = event.targetTouches[0].clientY;
  }
  if (params.mouseX === undefined) {params.mouseX = 0;}
  if (params.mouseY === undefined) {params.mouseY = 0;}
  params.frameLeft = links.Timeline.getAbsoluteLeft(this.dom.content);
  params.frameTop = links.Timeline.getAbsoluteTop(this.dom.content);
  params.previousLeft = 0;
  params.previousOffset = 0;

  params.moved = false;
  params.start = new Date(this.start);
  params.end = new Date(this.end);

  params.target = links.Timeline.getTarget(event);
  params.itemDragLeft = (params.target === this.dom.items.dragLeft);
  params.itemDragRight = (params.target === this.dom.items.dragRight);

  if (params.itemDragLeft || params.itemDragRight) {
    params.itemIndex = this.selection ? this.selection.index : undefined;
  }
  else {
    params.itemIndex = this.getItemIndex(params.target);
  }

  params.customTime = (params.target === dom.customTime ||
    params.target.parentNode === dom.customTime) ?
    this.customTime :
    undefined;

  params.addItem = (options.editable && event.ctrlKey);
  if (params.addItem) {
    // create a new event at the current mouse position
    var x = params.mouseX - params.frameLeft;
    var y = params.mouseY - params.frameTop;

    var xstart = this.screenToTime(x);
    if (options.snapEvents) {
      this.step.snap(xstart);
    }
    var xend = new Date(xstart);
    var content = "New";
    var group = this.getGroupFromHeight(y);
    this.addItem({
      'start': xstart,
      'end': xend,
      'content': content,
      'group': group.content
    });
    params.itemIndex = (this.items.length - 1);
    this.selectItem(params.itemIndex);
    params.itemDragRight = true;
  }

  params.editItem = options.editable ? this.isSelected(params.itemIndex) : undefined;
  if (params.editItem) {
    var item = this.items[params.itemIndex];
    params.itemStart = item.start;
    params.itemEnd = item.end;
    params.itemType = item.type;
    if (params.itemType == 'range') {
      params.itemLeft = this.timeToScreen(item.start);
      params.itemRight = this.timeToScreen(item.end);
    }
    else {
      params.itemLeft = this.timeToScreen(item.start);
    }
  }
  else {
    this.dom.frame.style.cursor = 'move';
  }
  if (!params.touchDown) {
    // add event listeners to handle moving the contents
    // we store the function onmousemove and onmouseup in the timeline, so we can
    // remove the eventlisteners lateron in the function mouseUp()
    var me = this;
    if (!params.onMouseMove) {
      params.onMouseMove = function (event) {me.onMouseMove(event);};
      links.Timeline.addEventListener(document, "mousemove", params.onMouseMove);
    }
    if (!params.onMouseUp) {
      params.onMouseUp = function (event) {me.onMouseUp(event);};
      links.Timeline.addEventListener(document, "mouseup", params.onMouseUp);
    }

    links.Timeline.preventDefault(event);
  }
}


/**
 * Perform moving operating.
 * This function activated from within the funcion links.Timeline.onMouseDown().
 * @param {event}   event  Well, eehh, the event
 */
links.Timeline.prototype.onMouseMove = function (event) {
  event = event || window.event;

  var params = this.eventParams,
    size = this.size,
    dom = this.dom,
    options = this.options;

  // calculate change in mouse position
  if (!params.touchDown) {
    var mouseX = event.clientX;
    var mouseY = event.clientY;
  }
  else {
    var mouseX = event.targetTouches[0].clientX;
    var mouseY = event.targetTouches[0].clientY;
  }
  if (mouseX === undefined) {mouseX = 0;}
  if (mouseY === undefined) {mouseY = 0;}

  if (params.mouseX === undefined) {
    params.mouseX = mouseX;
  }
  if (params.mouseY === undefined) {
    params.mouseY = mouseY;
  }

  var diffX = parseFloat(mouseX) - params.mouseX;
  var diffY = parseFloat(mouseY) - params.mouseY;

  params.moved = true;

  if (params.customTime) {
    var x = this.timeToScreen(params.customTime);
    var xnew = x + diffX;
    this.customTime = this.screenToTime(xnew);
    this.redrawCustomTime();

    // fire a timechange event
    this.trigger('timechange');
  }
  else if (params.editItem) {
    var item = this.items[params.itemIndex],
      domItem = item.dom,
      left,
      right;

    if (params.itemDragLeft) {
      // move the start of the item
      left = params.itemLeft + diffX;
      right = params.itemRight;

      item.start = this.screenToTime(left);
      if (options.snapEvents) {
        this.step.snap(item.start);
        left = this.timeToScreen(item.start);
      }

      if (left > right) {
        left = right;
        item.start = this.screenToTime(left);
      }
    }
    else if (params.itemDragRight) {
      // move the end of the item
      left = params.itemLeft;
      right = params.itemRight + diffX;

      item.end = this.screenToTime(right);
      if (options.snapEvents) {
        this.step.snap(item.end);
        right = this.timeToScreen(item.end);
      }

      if (right < left) {
        right = left;
        item.end = this.screenToTime(right);
      }
    }
    else {
      // move the item
      left = params.itemLeft + diffX;
      item.start = this.screenToTime(left);
      if (options.snapEvents) {
        this.step.snap(item.start);
        left = this.timeToScreen(item.start);
      }

      if (item.end) {
        right = left + (params.itemRight - params.itemLeft);
        item.end = this.screenToTime(right);
      }
    }

    switch(item.type) {
      case 'range':
        domItem.style.left = left + "px";
        //domItem.style.width = Math.max(right - left - 2 * item.borderWidth, 1) + "px";  // TODO
        domItem.style.width = Math.max(right - left, 1) + "px";
        break;

      case 'box':
        domItem.style.left = (left - item.width / 2) + "px";
        domItem.line.style.left = (left - item.lineWidth / 2) + "px";
        domItem.dot.style.left = (left - item.dotWidth / 2) + "px";
        break;

      case 'dot':
        domItem.style.left = (left - item.dotWidth / 2) + "px";
        break;
    }

    if (this.groups.length == 0) {
      // TODO: does not work well in FF, forces redraw with every mouse move it seems
      this.stackEvents(options.animate);
      if (!options.animate) {
        this.redrawFrame();
      }
      // Note: when animate==true, no redraw is needed here, its done by stackEvents animation
    }
    else {
      /* TODO: move item from one group to another when needed
      var y = mouseY - params.frameTop;
      var group = this.getGroupFromHeight(y);
      if (item.group !== group) {
        // ... move item to the other group
      }
      */
    }

    this.redrawDeleteButton();
    this.redrawDragAreas();
  }
  else if (options.moveable) {
    var interval = (params.end.valueOf() - params.start.valueOf());
    var diffMillisecs = parseFloat(-diffX) / size.contentWidth * interval;
    this.start = new Date(params.start.valueOf() + Math.round(diffMillisecs));
    this.end = new Date(params.end.valueOf() + Math.round(diffMillisecs));

    this.recalcConversion();

    // move the items by changing the left position of their frame.
    // this is much faster than repositioning all elements individually via the
    // redrawFrame() function (which is done once at mouseup)
    // note that we round diffX to prevent wrong positioning on millisecond scale
    var previousLeft = params.previousLeft || 0;
    var currentLeft = parseFloat(dom.items.frame.style.left) || 0;
    var previousOffset = params.previousOffset || 0;
    var frameOffset = previousOffset + (currentLeft - previousLeft);
    var frameLeft = -Math.round(diffMillisecs) / interval * size.contentWidth + frameOffset;
    params.previousOffset = frameOffset;
    params.previousLeft = frameLeft;

    dom.items.frame.style.left = (frameLeft) + "px";

    this.redrawCurrentTime();
    this.redrawCustomTime();
    this.redrawAxis();

    // fire a rangechange event
    this.trigger('rangechange');
  }

  links.Timeline.preventDefault(event);
}


/**
 * Stop moving operating.
 * This function activated from within the funcion links.Timeline.onMouseDown().
 * @param {event}  event   The event
 */
links.Timeline.prototype.onMouseUp = function (event) {
  var params = this.eventParams,
    options = this.options;

  event = event || window.event;

  this.dom.frame.style.cursor = 'auto';

  // remove event listeners here, important for Safari
  if (params.onMouseMove) {
    links.Timeline.removeEventListener(document, "mousemove", params.onMouseMove);
    delete params.onMouseMove;
  }
  if (params.onMouseUp) {
    links.Timeline.removeEventListener(document, "mouseup",   params.onMouseUp);
    delete params.onMouseUp;
  }
  //links.Timeline.preventDefault(event);

  if (params.customTime) {
    // fire a timechanged event
    this.trigger('timechanged');
  }
  else if (params.editItem) {
    var item = this.items[params.itemIndex];

    if (params.moved || params.addItem) {
      this.applyChange = true;
      this.applyAdd = true;

      this.updateData(params.itemIndex, {
        'start': item.start,
        'end': item.end
      });

      // fire an add or change event.
      // Note that the change can be canceled from within an event listener if
      // this listener calls the method cancelChange().
      this.trigger(params.addItem ? 'add' : 'change');

      if (params.addItem) {
        if (this.applyAdd) {
          this.updateData(params.itemIndex, {
            'start': item.start,
            'end': item.end,
            'content': item.content,
            'group': item.group ? item.group.content : undefined
          });
        }
        else {
          // undo an add
          this.deleteItem(params.itemIndex);
        }
      }
      else {
        if (this.applyChange) {
          this.updateData(params.itemIndex, {
            'start': item.start,
            'end': item.end
          });
        }
        else {
          // undo a change
          delete this.applyChange;
          delete this.applyAdd;

          var item = this.items[params.itemIndex],
            domItem = item.dom;

          item.start = params.itemStart;
          item.end = params.itemEnd;
          domItem.style.left = params.itemLeft + "px";
          domItem.style.width = (params.itemRight - params.itemLeft) + "px";
        }
      }

      this.recalcSize();
      this.stackEvents(options.animate);
      if (!options.animate) {
        this.redrawFrame();
      }
      this.redrawDeleteButton();
      this.redrawDragAreas();
    }
  }
  else {
    if (!params.moved && !params.zoomed) {
      // mouse did not move -> user has selected an item

      if (options.editable && (params.target === this.dom.items.deleteButton)) {
        // delete item
        if (this.selection) {
          this.confirmDeleteItem(this.selection.index);
        }
        this.redrawFrame();
      }
      else if (options.selectable) {
        // select/unselect item
        if (params.itemIndex !== undefined) {
          if (!this.isSelected(params.itemIndex)) {
            this.selectItem(params.itemIndex);
            this.trigger('select');
          }
        }
        else {
          this.unselectItem();
        }
        this.redrawDeleteButton();
      }
    }
    else {
      // timeline is moved
      this.redrawFrame();

      if ((params.moved && options.moveable) || (params.zoomed && options.zoomable) ) {
        // fire a rangechanged event
        this.trigger('rangechanged');
      }
    }
  }
}

/**
 * Double click event occurred for an item
 * @param {event}  event
 */
links.Timeline.prototype.onDblClick = function (event) {
  var params = this.eventParams,
    options = this.options,
    dom = this.dom,
    size = this.size;
  event = event || window.event;

  if (!options.editable) {
    return;
  }

  if (params.itemIndex !== undefined) {
    // fire the edit event
    this.trigger('edit');
  }
  else {
    // create a new item
    var x = event.clientX - links.Timeline.getAbsoluteLeft(dom.content);
    var y = event.clientY - links.Timeline.getAbsoluteTop(dom.content);

    // create a new event at the current mouse position
    var xstart = this.screenToTime(x);
    var xend = this.screenToTime(x  + size.frameWidth / 10); // add 10% of timeline width
    if (options.snapEvents) {
      this.step.snap(xstart);
      this.step.snap(xend);
    }

    var content = "New";
    var group = this.getGroupFromHeight(y);   // (group may be undefined)
    this.addItem({
      'start': xstart,
      'end': xend,
      'content': content,
      'group': group.content
    });
    params.itemIndex = (this.items.length - 1);
    this.selectItem(params.itemIndex);

    this.applyAdd = true;

    // fire an add event.
    // Note that the change can be canceled from within an event listener if
    // this listener calls the method cancelAdd().
    this.trigger('add');

    if (!this.applyAdd) {
      // undo an add
      this.deleteItem(params.itemIndex);
    }

    this.redrawDeleteButton();
    this.redrawDragAreas();
  }

  links.Timeline.preventDefault(event);
}


/**
 * Event handler for mouse wheel event, used to zoom the timeline
 * Code from http://adomas.org/javascript-mouse-wheel/
 * @param {event}  event   The event
 */
links.Timeline.prototype.onMouseWheel = function(event) {
  if (!this.options.zoomable)
    return;

  if (!event) { /* For IE. */
    event = window.event;
  }

  // retrieve delta
  var delta = 0;
  if (event.wheelDelta) { /* IE/Opera. */
    delta = event.wheelDelta/120;
  } else if (event.detail) { /* Mozilla case. */
    // In Mozilla, sign of delta is different than in IE.
    // Also, delta is multiple of 3.
    delta = -event.detail/3;
  }

  // If delta is nonzero, handle it.
  // Basically, delta is now positive if wheel was scrolled up,
  // and negative, if wheel was scrolled down.
  if (delta) {
    // TODO: on FireFox, the window is not redrawn within repeated scroll-events
    // -> use a delayed redraw? Make a zoom queue?

    var timeline = this;
    var zoom = function () {
      // check if frame is not resized (causing a mismatch with the end date)
      timeline.recalcSize();

      // perform the zoom action. Delta is normally 1 or -1
      var zoomFactor = delta / 5.0;
      var frameLeft = links.Timeline.getAbsoluteLeft(timeline.dom.content);
      var zoomAroundDate =
        (event.clientX != undefined && frameLeft != undefined) ?
        timeline.screenToTime(event.clientX - frameLeft) :
        undefined;

      timeline.zoom(zoomFactor, zoomAroundDate);

      // fire a rangechange and a rangechanged event
      timeline.trigger("rangechange");
      timeline.trigger("rangechanged");

      /* TODO: smooth scrolling on FF
      timeline.zooming = false;

      if (timeline.zoomingQueue) {
        setTimeout(timeline.zoomingQueue, 100);
        timeline.zoomingQueue = undefined;
      }

      timeline.zoomCount = (timeline.zoomCount || 0) + 1;
      console.log('zoomCount', timeline.zoomCount)
      */
    };

    zoom();

    /* TODO: smooth scrolling on FF
    if (!timeline.zooming || true) {

      timeline.zooming = true;
      setTimeout(zoom, 100);
    }
    else {
      timeline.zoomingQueue = zoom;
    }
    //*/
  }

  // Prevent default actions caused by mouse wheel.
  // That might be ugly, but we handle scrolls somehow
  // anyway, so don't bother here...
  links.Timeline.preventDefault(event);
}


/**
 * Zoom the timeline the given zoomfactor in or out. Start and end date will
 * be adjusted, and the timeline will be redrawn. You can optionally give a
 * date around which to zoom.
 * For example, try zoomfactor = 0.1 or -0.1
 * @param {float}  zoomFactor      Zooming amount. Positive value will zoom in,
 *                                 negative value will zoom out
 * @param {Date}   zoomAroundDate  Date around which will be zoomed. Optional
 */
links.Timeline.prototype.zoom = function(zoomFactor, zoomAroundDate) {
  // if zoomAroundDate is not provided, take it half between start Date and end Date
  if (zoomAroundDate == undefined) {
    zoomAroundDate = new Date((this.start.valueOf() + this.end.valueOf()) / 2);
  }

  // prevent zoom factor larger than 1 or smaller than -1 (larger than 1 will
  // result in a start>=end )
  if (zoomFactor >= 1) {
    zoomFactor = 0.9;
  }
  if (zoomFactor <= -1) {
    zoomFactor = -0.9;
  }

  // adjust a negative factor such that zooming in with 0.1 equals zooming
  // out with a factor -0.1
  if (zoomFactor < 0) {
    zoomFactor = zoomFactor / (1 + zoomFactor);
  }

  // zoom start Date and end Date relative to the zoomAroundDate
  var startDiff = parseFloat(this.start.valueOf() - zoomAroundDate.valueOf());
  var endDiff = parseFloat(this.end.valueOf() - zoomAroundDate.valueOf());

  // calculate new dates
  var newStart = new Date(this.start.valueOf() - startDiff * zoomFactor);
  var newEnd   = new Date(this.end.valueOf() - endDiff * zoomFactor);

  // prevent scale of less than 10 milliseconds
  // TODO: IE has problems with milliseconds
  if (zoomFactor > 0 && (newEnd.valueOf() - newStart.valueOf()) < 10) {
    return;
  }

  // prevent scale of more than than 10 thousand years
  if (zoomFactor < 0 && (newEnd.getFullYear() - newStart.getFullYear()) > 10000) {
    return;
  }

  // apply new dates
  this.start = newStart;
  this.end = newEnd;

  this.recalcSize();
  var animate = this.options.animate ? this.options.animateZoom : false;
  this.stackEvents(animate);
  if (!animate || this.groups.length > 0) {
    this.redrawFrame();
  }
  /* TODO
  else {
    this.redrawFrame();
    this.recalcSize();
    this.stackEvents(animate);
    this.redrawFrame();
  }*/
}


/**
 * Move the timeline the given movefactor to the left or right. Start and end
 * date will be adjusted, and the timeline will be redrawn.
 * For example, try moveFactor = 0.1 or -0.1
 * @param {float}  moveFactor      Moving amount. Positive value will move right,
 *                                 negative value will move left
 */
links.Timeline.prototype.move = function(moveFactor) {
  // zoom start Date and end Date relative to the zoomAroundDate
  var diff = parseFloat(this.end.valueOf() - this.start.valueOf());

  // apply new dates
  this.start = new Date(this.start.valueOf() + diff * moveFactor);
  this.end   = new Date(this.end.valueOf() + diff * moveFactor);

  this.recalcConversion();
  this.redrawFrame();
}

/**
 * Delete an item after a confirmation.
 * The deletion can be cancelled by executing .cancelDelete() during the
 * triggered event 'delete'.
 * @param {int} index   Index of the item to be deleted
 */
links.Timeline.prototype.confirmDeleteItem = function(index) {
  this.applyDelete = true;

  // select the event to be deleted
  if (!this.isSelected(index)) {
    this.selectItem(index);
  }

  // fire a delete event trigger.
  // Note that the delete event can be canceled from within an event listener if
  // this listener calls the method cancelChange().
  this.trigger('delete');

  if (this.applyDelete) {
    this.deleteItem(index);
  }

  delete this.applyDelete;
}

/**
 * Delete an item
 * @param {int} index   Index of the item to be deleted
 */
links.Timeline.prototype.deleteItem = function(index) {
  if (index >= this.items.length) {
    throw "Cannot delete row, index out of range";
  }

  this.unselectItem();

  // actually delete the item
  this.items.splice(index, 1);

  // delete the row in the original data table
  if (this.data) {
    if (google && google.visualization &&
        this.data instanceof google.visualization.DataTable) {
      this.data.removeRow(index);
    }
    else if (links.Timeline.isArray(this.data)) {
      this.data.splice(index, 1);
    }
    else {
      throw "Cannot delete row from data, unknown data type";
    }
  }

  this.size.dataChanged = true;
  this.redrawFrame();
  this.recalcSize();
  this.stackEvents(this.options.animate);
  if (!this.options.animate) {
    this.redrawFrame();
  }
  this.size.dataChanged = false;
}


/**
 * Delete all items
 */
links.Timeline.prototype.deleteAllItems = function() {
  this.unselectItem();

  // delete the loaded data
  this.items = [];

  // delete the groups
  this.deleteGroups();

  // empty original data table
  if (this.data) {
    if (google && google.visualization &&
        this.data instanceof google.visualization.DataTable) {
      this.data.removeRows(0, this.data.getNumberOfRows());
    }
    else if (links.Timeline.isArray(this.data)) {
      this.data.splice(0, this.data.length);
    }
    else {
      throw "Cannot delete row from data, unknown data type";
    }
  }

  this.size.dataChanged = true;
  this.redrawFrame();
  this.recalcSize();
  this.stackEvents(this.options.animate);
  if (!this.options.animate) {
    this.redrawFrame();
  }
  this.size.dataChanged = false;

}


/**
 * Find the group from a given height in the timeline
 * @param {Number} height   Height in the timeline
 * @return {Object} group   The group object, or undefined if out of range
 */
links.Timeline.prototype.getGroupFromHeight = function(height) {
  var groups = this.groups,
    options = this.options,
    size = this.size,
    y = height - (options.axisOnTop ? size.axis.height : 0);

  if (groups) {
    var group;
    for (var i = 0, iMax = groups.length; i < iMax; i++) {
      group = groups[i];
      if (y > group.top && y < group.top + group.height) {
        return group;
      }
    }

    return group; // return the last group
  }

  return undefined;
}

/**
 * Retrieve the properties of an item.
 * @param {Number} index
 * @return {Object} properties   Object containing item properties:<br>
 *                              {Date} start (required),
 *                              {Date} end (optional),
 *                              {String} content (required),
 *                              {String} group (optional)
 */
links.Timeline.prototype.getItem = function (index) {
  if (index >= this.items.length) {
    throw "Cannot get item, index out of range";
  }

  var item = this.items[index];

  var properties = {};
  properties.start = new Date(item.start);
  if (item.end) {
    properties.end = new Date(item.end);
  }
  properties.content = item.content;
  if (item.group) {
    properties.group = item.group.content;
  }

  return properties;
}

/**
 * Add a new item.
 * @param {Object} itemData     Object containing item properties:<br>
 *                              {Date} start (required),
 *                              {Date} end (optional),
 *                              {String} content (required),
 *                              {String} group (optional)
 */
links.Timeline.prototype.addItem = function (itemData) {
  var items = [
    itemData
  ];

  this.addItems(items);
}

/**
 * Add new items.
 * @param {Array} items  An array containing Objects.
 *                       The objects must have the following parameters:
 *                         {Date} start,
 *                         {Date} end,
 *                         {String} content with text or HTML code,
 *                         {String} group
 */
links.Timeline.prototype.addItems = function (items) {
  var newItems = items,
    curItems = this.items,
    groups = this.groups,
    groupIndexes = this.groupIndexes;
  // append the items
  for (var i = 0, iMax = newItems.length; i < iMax; i++) {
    var itemData = items[i];
    this.addGroup(itemData.group);

    curItems.push(this.createItem(itemData));
    var index = curItems.length - 1;
    this.updateData(index, itemData);
  }

  // redraw timeline
  this.size.dataChanged = true;
  this.redrawFrame();
  this.recalcSize();
  this.stackEvents(false);
  this.redrawFrame();
  this.size.dataChanged = false;
}

/**
 * Create an item object, containing all needed parameters
 * @param {Object} itemData  Object containing parameters start, end
 *                           content, group.
 * @return {Object} item
 */
links.Timeline.prototype.createItem = function(itemData) {
  var item = {
    'start': itemData.start,
    'end': itemData.end,
    'content': itemData.content,
    'type': itemData.end ? 'range' : this.options.style,
    'group': this.findGroup(itemData.group),
    'top': 0,
    'left': 0,
    'width': 0,
    'height': 0,
    'lineWidth' : 0,
    'dotWidth': 0,
    'dotHeight': 0
  };
  return item;
}

/**
 * Edit an item
 * @param {Number} index
 * @param {Object} itemData     Object containing item properties:<br>
 *                              {Date} start (required),
 *                              {Date} end (optional),
 *                              {String} content (required),
 *                              {String} group (optional)
 */
links.Timeline.prototype.changeItem = function (index, itemData) {
  if (index >= this.items.length) {
    throw "Cannot change item, index out of range";
  }

  var style = this.options.style;
  var item = this.items[index];

  // edit the item
  if (itemData.start) {
    item.start = itemData.start;
  }
  if (itemData.end) {
    item.end = itemData.end;
  }
  if (itemData.content) {
    item.content = itemData.content;
  }
  if (itemData.group) {
    item.group = this.addGroup(itemData.group);
  }

  // update the original data table
  this.updateData(index, itemData);

  // redraw timeline
  this.size.dataChanged = true;
  this.redrawFrame();
  this.recalcSize();
  this.stackEvents(false);
  this.redrawFrame();
  this.size.dataChanged = false;
}


/**
 * Find a group by its name.
 * @param {String} group
 * @return {Object} a group object or undefined when group is not found
 */
links.Timeline.prototype.findGroup = function (group) {
  var index = this.groupIndexes[group];
  return (index != undefined) ? this.groups[index] : undefined;
}

/**
 * Delete all groups
 */
links.Timeline.prototype.deleteGroups = function () {
  this.groups = [];
  this.groupIndexes = {};
}


/**
 * Add a group. When the group already exists, no new group is created
 * but the existing group is returned.
 * @param {String} groupName   the name of the group
 * @return {Object} groupObject
 */
links.Timeline.prototype.addGroup = function (groupName) {
  var groups = this.groups,
    groupIndexes = this.groupIndexes;

  var groupObj = groupIndexes[groupName];
  if (groupObj === undefined && groupName !== undefined) {
    var groupObj = {
      'content': groupName,
      'labelTop': 0,
      'lineTop': 0
      // note: this object will lateron get addition information,
      //       such as height and width of the group
    };
    groups.push(groupObj);

    // sort the groups
    if (this.options.axisOnTop) {
      groups.sort(function (a, b) {
        return a.content > b.content;
      });
    }
    else {
      groups.sort(function (a, b) {
        return a.content < b.content;
      });
    }

    // rebuilt the groupIndexes
    for (var i = 0, iMax = groups.length; i < iMax; i++) {
      groupIndexes[groups[i].content] = i;
    }
  }

  return groupObj;
}

/**
 * Cancel a change item
 * This method can be called insed an event listener which catches the "change"
 * event. The changed event position will be undone.
 */
links.Timeline.prototype.cancelChange = function () {
  this.applyChange = false;
}

/**
 * Cancel deletion of an item
 * This method can be called insed an event listener which catches the "delete"
 * event. Deletion of the event will be undone.
 */
links.Timeline.prototype.cancelDelete = function () {
  this.applyDelete = false;
}


/**
 * Cancel creation of a new item
 * This method can be called insed an event listener which catches the "new"
 * event. Creation of the new the event will be undone.
 */
links.Timeline.prototype.cancelAdd = function () {
  this.applyAdd = false;
}


/**
 * Select an event. The visible chart range will be moved such that the selected
 * event is placed in the middle.
 * For example selection = [{row: 5}];
 * @param {array} sel   An array with a column row, containing the row number
 *                      (the id) of the event to be selected.
 * @return {boolean}    true if selection is succesfully set, else false.
 */
links.Timeline.prototype.setSelection = function(selection) {
  if (selection != undefined && selection.length > 0) {
    if (selection[0].row != undefined) {
      var index = selection[0].row;
      if (this.items[index]) {
        var item = this.items[index];
        this.selectItem(index);

        // move the visible chart range to the selected event.
        var start = item.start;
        var end = item.end;
        if (end != undefined) {
          var middle = new Date((end.valueOf() + start.valueOf()) / 2);
        } else {
          var middle = new Date(start);
        }
        var diff = (this.end.valueOf() - this.start.valueOf()),
          newStart = new Date(middle.valueOf() - diff/2),
          newEnd = new Date(middle.valueOf() + diff/2);

        this.setVisibleChartRange(newStart, newEnd);

        return true;
      }
    }
  }
  return false;
}

/**
 * Retrieve the currently selected event
 * @return {array} sel  An array with a column row, containing the row number
 *                      of the selected event. If there is no selection, an
 *                      empty array is returned.
 */
links.Timeline.prototype.getSelection = function() {
  var sel = [];
  if (this.selection) {
    sel.push({"row": this.selection.index});
  }
  return sel;
}


/**
 * Select an item by its index
 * @param {Number} index
 */
links.Timeline.prototype.selectItem = function(index) {
  this.unselectItem();

  this.selection = undefined;

  if (this.items[index] !== undefined) {
    var item = this.items[index],
      domItem = item.dom;

    this.selection = {
      'index': index,
      'item': domItem
    };

    if (this.options.editable) {
      domItem.style.cursor = 'move';
    }
    switch (item.type) {
      case 'range':
        domItem.className = "timeline-event timeline-event-selected timeline-event-range";
        break;
      case 'box':
        domItem.className = "timeline-event timeline-event-selected timeline-event-box";
        domItem.line.className = "timeline-event timeline-event-selected timeline-event-line";
        domItem.dot.className = "timeline-event timeline-event-selected timeline-event-dot";
        break;
      case 'dot':
        domItem.className = "timeline-event timeline-event-selected";
        domItem.dot.className = "timeline-event timeline-event-selected timeline-event-dot";
        break;
    }
  }
}

/**
 * Check if an item is currently selected
 * @param {Number} index
 * @return {boolean} true if row is selected, else false
 */
links.Timeline.prototype.isSelected = function (index) {
  return (this.selection && this.selection.index === index);
}

/**
 * Unselect the currently selected event (if any)
 */
links.Timeline.prototype.unselectItem = function() {
  if (this.selection) {
    var item = this.items[this.selection.index];

    if (item && item.dom) {
      var domItem = item.dom;
      domItem.style.cursor = '';
      switch (item.type) {
        case 'range':
          domItem.className = "timeline-event timeline-event-range";
          break;
        case 'box':
          domItem.className = "timeline-event timeline-event-box";
          domItem.line.className = "timeline-event timeline-event-line";
          domItem.dot.className = "timeline-event timeline-event-dot";
          break;
        case 'dot':
          domItem.className = "";
          domItem.dot.className = "timeline-event timeline-event-dot";
          break;
      }
    }
  }

  this.selection = undefined;
}


/**
 * Stack the items such that they don't overlap. The items will have a minimal
 * distance equal to options.eventMargin.
 * @param {boolean} animate     if animate is true, the items are moved to
 *                              their new position animated
 */
links.Timeline.prototype.stackEvents = function(animate) {
  if (this.options.stackEvents == false || this.groups.length > 0) {
    // under this conditions we refuse to stack the events
    return;
  }

  if (animate == undefined) {
    animate = false;
  }

  var sortedItems = this.stackOrder(this.items);
  var finalItems = this.stackCalculateFinal(sortedItems, animate);

  if (animate) {
    // move animated to the final positions
    var animation = this.animation;
    if (!animation) {
      animation = {};
      this.animation = animation;
    }
    animation.finalItems = finalItems;

    var timeline = this;
    var step = function () {
      var arrived = timeline.stackMoveOneStep(sortedItems, animation.finalItems);

      timeline.recalcSize();
      timeline.redrawFrame();

      if (!arrived) {
        animation.timer = setTimeout(step, 30);
      }
      else {
        delete animation.finalItems;
        delete animation.timer;
      }
    }

    if (!animation.timer) {
      animation.timer = setTimeout(step, 30);
    }
  }
  else {
    this.stackMoveToFinal(sortedItems, finalItems);
    this.recalcSize();
    //this.redraw(); // TODO: cleanup
  }
}


/**
 * Order the items in the array this.items. The order is determined via:
 * - Ranges go before boxes and dots.
 * - The item with the left most location goes first
 * @param {Array} items        Array with items
 * @return {Array} sortedItems Array with sorted items
 */
links.Timeline.prototype.stackOrder = function(items) {
  // TODO: store the sorted items, to have less work later on
  var sortedItems = items.concat([]);

  var f = function (a, b) {
    if (a.type == 'range' && b.type != 'range') {
      return -1;
    }

    if (a.type != 'range' && b.type == 'range') {
      return 1;
    }

    return (a.left - b.left);
  };

  sortedItems.sort(f);

  return sortedItems;
}

/**
 * Adjust vertical positions of the events such that they don't overlap each
 * other.
 */
links.Timeline.prototype.stackCalculateFinal = function(items) {
  var size = this.size,
    axisTop = size.axis.top,
    options = this.options,
    axisOnTop = options.axisOnTop,
    eventMargin = options.eventMargin,
    eventMarginAxis = options.eventMarginAxis,
    finalItems = [];

  // initialize final positions
  for (var i = 0, iMax = items.length; i < iMax; i++) {
    var item = items[i],
      top,
      left,
      right,
      bottom,
      height = item.height,
      width = item.width;

    if (axisOnTop) {
      top = axisTop + eventMarginAxis + eventMargin / 2;
    }
    else {
      top = axisTop - height - eventMarginAxis - eventMargin / 2;
    }
    bottom = top + height;

    switch (item.type) {
      case 'range':
      case 'dot':
        left = this.timeToScreen(item.start);
        right = item.end ? this.timeToScreen(item.end) : left + width;
        break;

      case 'box':
        left = this.timeToScreen(item.start) - width / 2;
        right = left + width;
        break;
    }

    finalItems[i] = {
      'left': left,
      'top': top,
      'right': right,
      'bottom': bottom,
      'height': height,
      'item': item
    };
  }

  // calculate new, non-overlapping positions
  //var items = sortedItems;
  for (var i = 0, iMax = finalItems.length; i < iMax; i++) {
  //for (var i = finalItems.length - 1; i >= 0; i--) {
    var finalItem = finalItems[i];
    var collidingItem = null;
    do {
      // TODO: optimize checking for overlap. when there is a gap without items,
      //  you only need to check for items from the next item on, not from zero
      collidingItem = this.stackEventsCheckOverlap(finalItems, i, 0, i-1);
      if (collidingItem != null) {
        // There is a collision. Reposition the event above the colliding element
        if (axisOnTop) {
          finalItem.top = collidingItem.top + collidingItem.height + eventMargin;
        }
        else {
          finalItem.top = collidingItem.top - finalItem.height - eventMargin;
        }
        finalItem.bottom = finalItem.top + finalItem.height;
      }
    } while (collidingItem);
  }

  return finalItems;
}


/**
 * Move the events one step in the direction of their final positions
 * @param {Array} currentItems   Array with the real items and their current
 *                               positions
 * @param {Array} finalItems     Array with objects containing the final
 *                               positions of the items
 * @return {boolean} arrived     True if all items have reached their final
 *                               location, else false
 */
links.Timeline.prototype.stackMoveOneStep = function(currentItems, finalItems) {
  // TODO: check this method
  var arrived = true;

  // apply new positions animated
  for (i = 0, iMax = currentItems.length; i < iMax; i++) {
    var finalItem = finalItems[i],
      item = finalItem.item;

    var topNow = parseInt(item.top);
    var topFinal = parseInt(finalItem.top);
    var diff = (topFinal - topNow);
    if (diff) {
      var step = (topFinal == topNow) ? 0 : ((topFinal > topNow) ? 1 : -1);
      if (Math.abs(diff) > 4) step = diff / 4;
      var topNew = parseInt(topNow + step);

      if (topNew != topFinal) {
        arrived = false;
      }

      item.top = topNew;
      item.bottom = item.top + item.height;
    }
    else {
      item.top = finalItem.top;
      item.bottom = finalItem.bottom;
    }

    item.left = finalItem.left;
    item.right = finalItem.right;
  }

  return arrived;
}



/**
 * Move the events from their current position to the final position
 * @param {Array} currentItems   Array with the real items and their current
 *                               positions
 * @param {Array} finalItems     Array with objects containing the final
 *                               positions of the items
 */
links.Timeline.prototype.stackMoveToFinal = function(currentItems, finalItems) {
  // Put the events directly at there final position
  for (i = 0, iMax = currentItems.length; i < iMax; i++) {
    var current = currentItems[i],
      finalItem = finalItems[i];

    current.left = finalItem.left;
    current.top = finalItem.top;
    current.right = finalItem.right;
    current.bottom = finalItem.bottom;
  }
}



/**
 * Check if the destiny position of given item overlaps with any
 * of the other items from index itemStart to itemEnd.
 * @param {Array} items      Array with items
 * @param {int}  itemIndex   Number of the item to be checked for overlap
 * @param {int}  itemStart   First item to be checked.
 * @param {int}  itemEnd     Last item to be checked.
 * @return {Object}          colliding item, or undefined when no collisions
 */
links.Timeline.prototype.stackEventsCheckOverlap = function(items, itemIndex,
    itemStart, itemEnd) {
    eventMargin = this.options.eventMargin,
    collision = this.collision;

  /* TODO: cleanup
  var item1 = items[itemIndex];
  for (var i = itemStart; i <= itemEnd; i++) {
    var item2 = items[i];
    if (collision(item1, item2, eventMargin)) {
      if (i != itemIndex) {
        return item2;
      }
    }
  }
  return;
  //*/

  // we loop from end to start, as we suppose that the chance of a
  // collision is larger for items at the end, so check these first.
  var item1 = items[itemIndex];
  for (var i = itemEnd; i >= itemStart; i--) {
    var item2 = items[i];
    if (collision(item1, item2, eventMargin)) {
      if (i != itemIndex) {
        return item2;
      }
    }
  }
}

/**
 * Test if the two provided items collide
 * The items must have parameters left, right, top, and bottom.
 * @param {htmlelement} item1   The first item
 * @param {htmlelement} item2    The second item
 * @param {int}         margin  A minimum required margin. Optional.
 *                              If margin is provided, the two items will be
 *                              marked colliding when they overlap or
 *                              when the margin between the two is smaller than
 *                              the requested margin.
 * @return {boolean}            true if item1 and item2 collide, else false
 */
links.Timeline.prototype.collision = function(item1, item2, margin) {
  // set margin if not specified
  if (margin == undefined) {
    margin = 0;
  }

  // calculate if there is overlap (collision)
  return (item1.left - margin < item2.right &&
          item1.right + margin > item2.left &&
          item1.top - margin < item2.bottom &&
          item1.bottom + margin > item2.top);
}


/**
 * fire an event
 * @param {String} event   The name of an event, for example "rangechange" or "edit"
 */
links.Timeline.prototype.trigger = function (event) {
  // built up properties
  var properties = null;
  switch (event) {
    case 'rangechange':
    case 'rangechanged':
      properties = {
        'start': new Date(this.start),
        'end': new Date(this.end)
      };
      break;

    case 'timechange':
    case 'timechanged':
      properties = {
        'time': new Date(this.customTime)
      };
      break;
  }

  // trigger the links event bus
  links.events.trigger(this, event, properties);

  // trigger the google event bus
  if (google && google.visualization) {
    google.visualization.events.trigger(this, event, properties);
  }
}



/** ------------------------------------------------------------------------ **/


/**
 * Event listener (singleton)
 */
links.events = links.events || {
  'listeners': [],

  /**
   * Find a single listener by its object
   * @param {Object} object
   * @return {Number} index  -1 when not found
   */
  'indexOf': function (object) {
    var listeners = this.listeners;
    for (var i = 0, iMax = this.listeners.length; i < iMax; i++) {
      var listener = listeners[i];
      if (listener && listener.object == object) {
        return i;
      }
    }
    return -1;
  },

  /**
   * Add an event listener
   * @param {Object} object
   * @param {String} event       The name of an event, for example 'select'
   * @param {function} callback  The callback method, called when the
   *                             event takes place
   */
  'addListener': function (object, event, callback) {
    var index = this.indexOf(object);
    var listener = this.listeners[index];
    if (!listener) {
      listener = {
        'object': object,
        'events': {}
      };
      this.listeners.push(listener);
    }

    var callbacks = listener.events[event];
    if (!callbacks) {
      callbacks = [];
      listener.events[event] = callbacks;
    }

    // add the callback if it does not yet exist
    if (callbacks.indexOf(callback) == -1) {
      callbacks.push(callback);
    }
  },

  /**
   * Remove an event listener
   * @param {Object} object
   * @param {String} event       The name of an event, for example 'select'
   * @param {function} callback  The registered callback method
   */
  'removeListener': function (object, event, callback) {
    var index = this.indexOf(object);
    var listener = this.listeners[index];
    if (listener) {
      var callbacks = listener.events[event];
      if (callbacks) {
        var index = callbacks.indexOf(callback);
        if (index != -1) {
          callbacks.splice(index, 1);
        }

        // remove the array when empty
        if (callbacks.length == 0) {
          delete listener.events[event];
        }
      }

      // count the number of registered events. remove listener when empty
      var count = 0;
      var events = listener.events;
      for (var event in events) {
        if (events.hasOwnProperty(event)) {
          count++;
        }
      }
      if (count == 0) {
        delete this.listeners[index];
      }
    }
  },

  /**
   * Remove all registered event listeners
   */
  'removeAllListeners': function () {
    this.listeners = [];
  },

  /**
   * Trigger an event. All registered event handlers will be called
   * @param {Object} object
   * @param {String} event
   * @param {Object} properties (optional)
   */
  'trigger': function (object, event, properties) {
    var index = this.indexOf(object);
    var listener = this.listeners[index];
    if (listener) {
      var callbacks = listener.events[event];
      if (callbacks) {
        for (var i = 0, iMax = callbacks.length; i < iMax; i++) {
          callbacks[i](properties);
        }
      }
    }
  }
};


/** ------------------------------------------------------------------------ **/

/**
 * @class StepDate
 * The class StepDate is an iterator for dates. You provide a start date and an
 * end date. The class itself determines the best scale (step size) based on the
 * provided start Date, end Date, and minimumStep.
 *
 * If minimumStep is provided, the step size is chosen as close as possible
 * to the minimumStep but larger than minimumStep. If minimumStep is not
 * provided, the scale is set to 1 DAY.
 * The minimumStep should correspond with the onscreen size of about 6 characters
 *
 * Alternatively, you can set a scale by hand.
 * After creation, you can initialize the class by executing start(). Then you
 * can iterate from the start date to the end date via next(). You can check if
 * the end date is reached with the function end(). After each step, you can
 * retrieve the current date via get().
 * The class step has scales ranging from milliseconds, seconds, minutes, hours,
 * days, to years.
 *
 * Version: 0.9
 *
 * @param {Date} start        The start date, for example new Date(2010, 9, 21)
 *                            or new Date(2010, 9,21,23,45,00)
 * @param {Date} end          The end date
 * @param {int}  minimumStep  Optional. Minimum step size in milliseconds
 */
links.Timeline.StepDate = function(start, end, minimumStep) {

  // variables
  this.current = new Date();
  this._start = new Date();
  this._end = new Date();

  this.autoScale  = true;
  this.scale = links.Timeline.StepDate.SCALE.DAY;
  this.step = 1;

  // initialize the range
  this.setRange(start, end, minimumStep);
}

/// enum scale
links.Timeline.StepDate.SCALE = { MILLISECOND : 1,
                         SECOND : 2,
                         MINUTE : 3,
                         HOUR : 4,
                         DAY : 5,
                         MONTH : 6,
                         YEAR : 7};


/**
 * Set a new range
 * If minimumStep is provided, the step size is chosen as close as possible
 * to the minimumStep but larger than minimumStep. If minimumStep is not
 * provided, the scale is set to 1 DAY.
 * The minimumStep should correspond with the onscreen size of about 6 characters
 * @param {Date} start        The start date and time.
 * @param {Date} end          The end date and time.
 * @param {int}  minimumStep  Optional. Minimum step size in milliseconds
 */
links.Timeline.StepDate.prototype.setRange = function(start, end, minimumStep) {
  if (isNaN(start) || isNaN(end)) {
    //throw  "No legal start or end date in method setRange";
    return;
  }

  this._start      = (start != undefined)  ? new Date(start) : new Date();
  this._end        = (end != undefined)    ? new Date(end) : new Date();

  if (this.autoScale) {
    this.setMinimumStep(minimumStep);
  }
}

/**
 * Set the step iterator to the start date.
 */
links.Timeline.StepDate.prototype.start = function() {
  this.current = new Date(this._start);
  this.roundToMinor();
}

/**
 * Round the current date to the first minor date value
 * This must be executed once when the current date is set to start Date
 */
links.Timeline.StepDate.prototype.roundToMinor = function() {
  // round to floor
  // IMPORTANT: we have no breaks in this switch! (this is no bug)
  switch (this.scale) {
    case links.Timeline.StepDate.SCALE.YEAR:
      this.current.setFullYear(this.step * Math.floor(this.current.getFullYear() / this.step));
      this.current.setMonth(0);
    case links.Timeline.StepDate.SCALE.MONTH:        this.current.setDate(1);
    case links.Timeline.StepDate.SCALE.DAY:          this.current.setHours(0);
    case links.Timeline.StepDate.SCALE.HOUR:         this.current.setMinutes(0);
    case links.Timeline.StepDate.SCALE.MINUTE:       this.current.setSeconds(0);
    case links.Timeline.StepDate.SCALE.SECOND:       this.current.setMilliseconds(0);
    //case links.Timeline.StepDate.SCALE.MILLISECOND: // nothing to do for milliseconds
  }

  if (this.step != 1) {
    // round down to the first minor value that is a multiple of the current step size
    switch (this.scale) {
      case links.Timeline.StepDate.SCALE.MILLISECOND:  this.current.setMilliseconds(this.current.getMilliseconds() - this.current.getMilliseconds() % this.step);  break;
      case links.Timeline.StepDate.SCALE.SECOND:       this.current.setSeconds(this.current.getSeconds() - this.current.getSeconds() % this.step);  break;
      case links.Timeline.StepDate.SCALE.MINUTE:       this.current.setMinutes(this.current.getMinutes() - this.current.getMinutes() % this.step);  break;
      case links.Timeline.StepDate.SCALE.HOUR:         this.current.setHours(this.current.getHours() - this.current.getHours() % this.step);  break;
      case links.Timeline.StepDate.SCALE.DAY:          this.current.setDate((this.current.getDate()-1) - (this.current.getDate()-1) % this.step + 1);  break;
      case links.Timeline.StepDate.SCALE.MONTH:        this.current.setMonth(this.current.getMonth() - this.current.getMonth() % this.step);  break;
      case links.Timeline.StepDate.SCALE.YEAR:         this.current.setFullYear(this.current.getFullYear() - this.current.getFullYear() % this.step); break;
      default:                      break;
    }
  }
}

/**
 * Check if the end date is reached
 * @return {boolean}  true if the current date has passed the end date
 */
links.Timeline.StepDate.prototype.end = function () {
  return (this.current.getTime() > this._end.getTime());
}

/**
 * Do the next step
 */
links.Timeline.StepDate.prototype.next = function() {
  var prev = this.current.getTime();

  // Two cases, needed to prevent issues with switching daylight savings
  // (end of March and end of October)
  if (this.current.getMonth() < 6)   {
    switch (this.scale)
    {
      case links.Timeline.StepDate.SCALE.MILLISECOND:

      this.current = new Date(this.current.getTime() + this.step); break;
      case links.Timeline.StepDate.SCALE.SECOND:       this.current = new Date(this.current.getTime() + this.step * 1000); break;
      case links.Timeline.StepDate.SCALE.MINUTE:       this.current = new Date(this.current.getTime() + this.step * 1000 * 60); break;
      case links.Timeline.StepDate.SCALE.HOUR:
        this.current = new Date(this.current.getTime() + this.step * 1000 * 60 * 60);
        // in case of skipping an hour for daylight savings, adjust the hour again (else you get: 0h 5h 9h ... instead of 0h 4h 8h ...)
        var h = this.current.getHours();
        this.current.setHours(h - (h % this.step));
        break;
      case links.Timeline.StepDate.SCALE.DAY:          this.current.setDate(this.current.getDate() + this.step); break;
      case links.Timeline.StepDate.SCALE.MONTH:        this.current.setMonth(this.current.getMonth() + this.step); break;
      case links.Timeline.StepDate.SCALE.YEAR:         this.current.setFullYear(this.current.getFullYear() + this.step); break;
      default:                      break;
    }
  }
  else {
    switch (this.scale)
    {
      case links.Timeline.StepDate.SCALE.MILLISECOND:

      this.current = new Date(this.current.getTime() + this.step); break;
      case links.Timeline.StepDate.SCALE.SECOND:       this.current.setSeconds(this.current.getSeconds() + this.step); break;
      case links.Timeline.StepDate.SCALE.MINUTE:       this.current.setMinutes(this.current.getMinutes() + this.step); break;
      case links.Timeline.StepDate.SCALE.HOUR:         this.current.setHours(this.current.getHours() + this.step); break;
      case links.Timeline.StepDate.SCALE.DAY:          this.current.setDate(this.current.getDate() + this.step); break;
      case links.Timeline.StepDate.SCALE.MONTH:        this.current.setMonth(this.current.getMonth() + this.step); break;
      case links.Timeline.StepDate.SCALE.YEAR:         this.current.setFullYear(this.current.getFullYear() + this.step); break;
      default:                      break;
    }
  }

  if (this.step != 1) {
    // round down to the correct major value
    switch (this.scale) {
      case links.Timeline.StepDate.SCALE.MILLISECOND:  if(this.current.getMilliseconds() < this.step) this.current.setMilliseconds(0);  break;
      case links.Timeline.StepDate.SCALE.SECOND:       if(this.current.getSeconds() < this.step) this.current.setSeconds(0);  break;
      case links.Timeline.StepDate.SCALE.MINUTE:       if(this.current.getMinutes() < this.step) this.current.setMinutes(0);  break;
      case links.Timeline.StepDate.SCALE.HOUR:         if(this.current.getHours() < this.step) this.current.setHours(0);  break;
      case links.Timeline.StepDate.SCALE.DAY:          if(this.current.getDate() < this.step+1) this.current.setDate(1); break;
      case links.Timeline.StepDate.SCALE.MONTH:        if(this.current.getMonth() < this.step) this.current.setMonth(0);  break;
      case links.Timeline.StepDate.SCALE.YEAR:         break; // nothing to do for year
      default:                break;
    }
  }

  // safety mechanism: if current time is still unchanged, move to the end
  if (this.current.getTime() == prev) {
    this.current = new Date(this._end);
  }
}


/**
 * Get the current datetime
 * @return {Date}  current The current date
 */
links.Timeline.StepDate.prototype.getCurrent = function() {
  return this.current;
}

/**
 * Set a custom scale. Autoscaling will be disabled.
 * For example setScale(SCALE.MINUTES, 5) will result
 * in minor steps of 5 minutes, and major steps of an hour.
 *
 * @param {Step.SCALE} newScale  A scale. Choose from SCALE.MILLISECOND,
 *                               SCALE.SECOND, SCALE.MINUTE, SCALE.HOUR,
 *                               SCALE.DAY, SCALE.MONTH, SCALE.YEAR.
 * @param {int}        newStep   A step size, by default 1. Choose for
 *                               example 1, 2, 5, or 10.
 */
links.Timeline.StepDate.prototype.setScale = function(newScale, newStep) {
  this.scale = newScale;

  if (newStep > 0)
    this.step = newStep;

  this.autoScale = false;
}

/**
 * Enable or disable autoscaling
 * @param {boolean} enable  If true, autoascaling is set true
 */
links.Timeline.StepDate.prototype.setAutoScale = function (enable) {
  this.autoScale = enable;
}


/**
 * Automatically determine the scale that bests fits the provided minimum step
 * @param {int} minimumStep  The minimum step size in milliseconds
 */
links.Timeline.StepDate.prototype.setMinimumStep = function(minimumStep) {
  if (minimumStep == undefined)
    return;

  var stepYear       = (1000 * 60 * 60 * 24 * 30 * 12);
  var stepMonth      = (1000 * 60 * 60 * 24 * 30);
  var stepDay        = (1000 * 60 * 60 * 24);
  var stepHour       = (1000 * 60 * 60);
  var stepMinute     = (1000 * 60);
  var stepSecond     = (1000);
  var stepMillisecond= (1);

  // find the smallest step that is larger than the provided minimumStep
  if (stepYear*1000 > minimumStep)        {this.scale = links.Timeline.StepDate.SCALE.YEAR;        this.step = 1000;}
  if (stepYear*500 > minimumStep)         {this.scale = links.Timeline.StepDate.SCALE.YEAR;        this.step = 500;}
  if (stepYear*100 > minimumStep)         {this.scale = links.Timeline.StepDate.SCALE.YEAR;        this.step = 100;}
  if (stepYear*50 > minimumStep)          {this.scale = links.Timeline.StepDate.SCALE.YEAR;        this.step = 50;}
  if (stepYear*10 > minimumStep)          {this.scale = links.Timeline.StepDate.SCALE.YEAR;        this.step = 10;}
  if (stepYear*5 > minimumStep)           {this.scale = links.Timeline.StepDate.SCALE.YEAR;        this.step = 5;}
  if (stepYear > minimumStep)             {this.scale = links.Timeline.StepDate.SCALE.YEAR;        this.step = 1;}
  if (stepMonth*3 > minimumStep)          {this.scale = links.Timeline.StepDate.SCALE.MONTH;       this.step = 3;}
  if (stepMonth > minimumStep)            {this.scale = links.Timeline.StepDate.SCALE.MONTH;       this.step = 1;}
  if (stepDay*5 > minimumStep)            {this.scale = links.Timeline.StepDate.SCALE.DAY;         this.step = 5;}
  if (stepDay*2 > minimumStep)            {this.scale = links.Timeline.StepDate.SCALE.DAY;         this.step = 2;}
  if (stepDay > minimumStep)              {this.scale = links.Timeline.StepDate.SCALE.DAY;         this.step = 1;}
  if (stepHour*4 > minimumStep)           {this.scale = links.Timeline.StepDate.SCALE.HOUR;        this.step = 4;}
  if (stepHour > minimumStep)             {this.scale = links.Timeline.StepDate.SCALE.HOUR;        this.step = 1;}
  if (stepMinute*15 > minimumStep)        {this.scale = links.Timeline.StepDate.SCALE.MINUTE;      this.step = 15;}
  if (stepMinute*10 > minimumStep)        {this.scale = links.Timeline.StepDate.SCALE.MINUTE;      this.step = 10;}
  if (stepMinute*5 > minimumStep)         {this.scale = links.Timeline.StepDate.SCALE.MINUTE;      this.step = 5;}
  if (stepMinute > minimumStep)           {this.scale = links.Timeline.StepDate.SCALE.MINUTE;      this.step = 1;}
  if (stepSecond*15 > minimumStep)        {this.scale = links.Timeline.StepDate.SCALE.SECOND;      this.step = 15;}
  if (stepSecond*10 > minimumStep)        {this.scale = links.Timeline.StepDate.SCALE.SECOND;      this.step = 10;}
  if (stepSecond*5 > minimumStep)         {this.scale = links.Timeline.StepDate.SCALE.SECOND;      this.step = 5;}
  if (stepSecond > minimumStep)           {this.scale = links.Timeline.StepDate.SCALE.SECOND;      this.step = 1;}
  if (stepMillisecond*200 > minimumStep)  {this.scale = links.Timeline.StepDate.SCALE.MILLISECOND; this.step = 200;}
  if (stepMillisecond*100 > minimumStep)  {this.scale = links.Timeline.StepDate.SCALE.MILLISECOND; this.step = 100;}
  if (stepMillisecond*50 > minimumStep)   {this.scale = links.Timeline.StepDate.SCALE.MILLISECOND; this.step = 50;}
  if (stepMillisecond*10 > minimumStep)   {this.scale = links.Timeline.StepDate.SCALE.MILLISECOND; this.step = 10;}
  if (stepMillisecond*5 > minimumStep)    {this.scale = links.Timeline.StepDate.SCALE.MILLISECOND; this.step = 5;}
  if (stepMillisecond > minimumStep)      {this.scale = links.Timeline.StepDate.SCALE.MILLISECOND; this.step = 1;}
}

/**
 * Snap a date to a rounded value. The snap intervals are dependent on the
 * current scale and step.
 * @param {Date} date   the date to be snapped
 */
links.Timeline.StepDate.prototype.snap = function(date) {
  if (this.scale == links.Timeline.StepDate.SCALE.YEAR) {
    var year = date.getFullYear() + Math.round(date.getMonth() / 12);
    date.setFullYear(Math.round(year / this.step) * this.step);
    date.setMonth(0);
    date.setDate(0);
    date.setHours(0);
    date.setMinutes(0);
    date.setSeconds(0);
    date.setMilliseconds(0);
  }
  else if (this.scale == links.Timeline.StepDate.SCALE.MONTH) {
    if (date.getDate() > 15) {
      date.setDate(1);
      date.setMonth(date.getMonth() + 1);
      // important: first set Date to 1, after that change the month.
    }
    else {
      date.setDate(1);
    }

    date.setHours(0);
    date.setMinutes(0);
    date.setSeconds(0);
    date.setMilliseconds(0);
  }
  else if (this.scale == links.Timeline.StepDate.SCALE.DAY) {
    switch (this.step) {
      case 5:
      case 2:
        date.setHours(Math.round(date.getHours() / 24) * 24); break;
      default:
        date.setHours(Math.round(date.getHours() / 12) * 12); break;
    }
    date.setMinutes(0);
    date.setSeconds(0);
    date.setMilliseconds(0);
  }
  else if (this.scale == links.Timeline.StepDate.SCALE.HOUR) {
    switch (this.step) {
      case 4:
        date.setMinutes(Math.round(date.getMinutes() / 60) * 60); break;
      default:
        date.setMinutes(Math.round(date.getMinutes() / 30) * 30); break;
    }
    date.setSeconds(0);
    date.setMilliseconds(0);
  } else if (this.scale == links.Timeline.StepDate.SCALE.MINUTE) {
    switch (this.step) {
      case 15:
      case 10:
        date.setMinutes(Math.round(date.getMinutes() / 5) * 5);
        date.setSeconds(0);
        break;
      case 5:
        date.setSeconds(Math.round(date.getSeconds() / 60) * 60); break;
      default:
        date.setSeconds(Math.round(date.getSeconds() / 30) * 30); break;
    }
    date.setMilliseconds(0);
  }
  else if (this.scale == links.Timeline.StepDate.SCALE.SECOND) {
    switch (this.step) {
      case 15:
      case 10:
        date.setSeconds(Math.round(date.getSeconds() / 5) * 5);
        date.setMilliseconds(0);
        break;
      case 5:
        date.setMilliseconds(Math.round(date.getMilliseconds() / 1000) * 1000); break;
      default:
        date.setMilliseconds(Math.round(date.getMilliseconds() / 500) * 500); break;
    }
  }
  else if (this.scale == links.Timeline.StepDate.SCALE.MILLISECOND) {
    var step = this.step > 5 ? this.step / 2 : 1;
    date.setMilliseconds(Math.round(date.getMilliseconds() / step) * step);
  }
}

/**
 * Check if the current step is a major step (for example when the step
 * is DAY, a major step is each first day of the MONTH)
 * @return true if current date is major, else false.
 */
links.Timeline.StepDate.prototype.isMajor = function() {
  switch (this.scale)
  {
    case links.Timeline.StepDate.SCALE.MILLISECOND:
      return (this.current.getMilliseconds() == 0);
    case links.Timeline.StepDate.SCALE.SECOND:
      return (this.current.getSeconds() == 0);
    case links.Timeline.StepDate.SCALE.MINUTE:
      return (this.current.getHours() == 0) && (this.current.getMinutes() == 0);
      // Note: this is no bug. Major label is equal for both minute and hour scale
    case links.Timeline.StepDate.SCALE.HOUR:
      return (this.current.getHours() == 0);
    case links.Timeline.StepDate.SCALE.DAY:
      return (this.current.getDate() == 1);
    case links.Timeline.StepDate.SCALE.MONTH:
      return (this.current.getMonth() == 0);
    case links.Timeline.StepDate.SCALE.YEAR:
      return false
    default:
      return false;
  }
}


/**
 * Returns formatted text for the minor axislabel, depending on the current
 * date and the scale. For example when scale is MINUTE, the current time is
 * formatted as "hh:mm".
 * @param {Date}       optional custom date. if not provided, current date is taken
 * @return {string}    minor axislabel
 */
links.Timeline.StepDate.prototype.getLabelMinor = function(date) {
  var MONTHS_SHORT = new Array("Jan", "Feb", "Mar",
                                "Apr", "May", "Jun",
                                "Jul", "Aug", "Sep",
                                "Oct", "Nov", "Dec");

  if (date == undefined) {
    date = this.current;
  }

  switch (this.scale)
  {
    case links.Timeline.StepDate.SCALE.MILLISECOND:  return String(date.getMilliseconds());
    case links.Timeline.StepDate.SCALE.SECOND:       return String(date.getSeconds());
    case links.Timeline.StepDate.SCALE.MINUTE:       return this.addZeros(date.getHours(), 2) + ":" +
                                                       this.addZeros(date.getMinutes(), 2);
    case links.Timeline.StepDate.SCALE.HOUR:         return this.addZeros(date.getHours(), 2) + ":" +
                                                       this.addZeros(date.getMinutes(), 2);
    case links.Timeline.StepDate.SCALE.DAY:          return String(date.getDate());
    case links.Timeline.StepDate.SCALE.MONTH:        return MONTHS_SHORT[date.getMonth()];   // month is zero based
    case links.Timeline.StepDate.SCALE.YEAR:         return String(date.getFullYear());
    default:                                         return "";
  }
}


/**
 * Returns formatted text for the major axislabel, depending on the current
 * date and the scale. For example when scale is MINUTE, the major scale is
 * hours, and the hour will be formatted as "hh".
 * @param {Date}       optional custom date. if not provided, current date is taken
 * @return {string}    major axislabel
 */
links.Timeline.StepDate.prototype.getLabelMajor = function(date) {
  var MONTHS = new Array("January", "February", "March",
                         "April", "May", "June",
                         "July", "August", "September",
                         "October", "November", "December");
  var DAYS = new Array("Sunday", "Monday", "Tuesday",
                       "Wednesday", "Thursday", "Friday", "Saturday");

  if (date == undefined) {
    date = this.current;
  }

  switch (this.scale) {
    case links.Timeline.StepDate.SCALE.MILLISECOND:
      return  this.addZeros(date.getHours(), 2) + ":" +
              this.addZeros(date.getMinutes(), 2) + ":" +
              this.addZeros(date.getSeconds(), 2);
    case links.Timeline.StepDate.SCALE.SECOND:
      return  date.getDate() + " " +
              MONTHS[date.getMonth()] + " " +
              this.addZeros(date.getHours(), 2) + ":" +
              this.addZeros(date.getMinutes(), 2);
    case links.Timeline.StepDate.SCALE.MINUTE:
      return  DAYS[date.getDay()] + " " +
              date.getDate() + " " +
              MONTHS[date.getMonth()] + " " +
              date.getFullYear();
    case links.Timeline.StepDate.SCALE.HOUR:
      return  DAYS[date.getDay()] + " " +
              date.getDate() + " " +
              MONTHS[date.getMonth()] + " " +
              date.getFullYear();
    case links.Timeline.StepDate.SCALE.DAY:
      return  MONTHS[date.getMonth()] + " " +
              date.getFullYear();
    case links.Timeline.StepDate.SCALE.MONTH:
      return String(date.getFullYear());
    default:
      return "";
  }
}

/**
 * Add leading zeros to the given value to match the desired length.
 * For example addZeros(123, 5) returns "00123"
 * @param {int} value   A value
 * @param {int} len     Desired final length
 * @return {string}     value with leading zeros
 */
links.Timeline.StepDate.prototype.addZeros = function(value, len) {
  var str = "" + value;
  while (str.length < len) {
    str = "0" + str;
  }
  return str;
}



/** ------------------------------------------------------------------------ **/

/**
 * Image Loader service.
 * can be used to get a callback when a certain image is loaded
 *
 */
links.imageloader = (function () {
  var urls = {};  // the loaded urls
  var callbacks = {}; // the urls currently being loaded. Each key contains
                      // an array with callbacks

  /**
   * Check if an image url is loaded
   * @param {String} url
   * @return {boolean} loaded   True when loaded, false when not loaded
   *                            or when being loaded
   */
  function isLoaded (url) {
    if (urls[url] == true) {
      return true;
    }

    var image = new Image();
    image.src = url;
    if (image.complete) {
      return true;
    }

    return false;
  };


  /**
   * Check if an image url is being loaded
   * @param {String} url
   * @return {boolean} loading   True when being loaded, false when not loading
   *                             or when already loaded
   */
  function isLoading (url) {
    return (callbacks[url] != undefined);
  }

  /**
   * Load given image url
   * @param {String} url
   * @param {function} callback
   * @param {boolean} sendCallbackWhenAlreadyLoaded  optional
   */
  function load (url, callback, sendCallbackWhenAlreadyLoaded) {
    if (sendCallbackWhenAlreadyLoaded == undefined) {
      sendCallbackWhenAlreadyLoaded = true;
    }

    if (isLoaded(url)) {
      if (sendCallbackWhenAlreadyLoaded) {
        callback(url);
      }
      return;
    }

    if (isLoading(url) && !sendCallbackWhenAlreadyLoaded) {
      return;
    }

    var c = callbacks[url];
    if (!c) {
      var image = new Image();
      image.src = url;

      c = [];
      callbacks[url] = c;

      image.onload = function (event) {
        urls[url] = true;
        delete callbacks[url];

        for (var i = 0; i < c.length; i++) {
          c[i](url);
        }
      }
    }

    if (c.indexOf(callback) == -1) {
      c.push(callback);
    }
  };

  return {
    'isLoaded': isLoaded,
    'isLoading': isLoading,
    'load': load
  };
})();


/** ------------------------------------------------------------------------ **/


/**
 * Add and event listener. Works for all browsers
 * @param {DOM Element} element    An html element
 * @param {string}      action     The action, for example "click",
 *                                 without the prefix "on"
 * @param {function}    listener   The callback function to be executed
 * @param {boolean}     useCapture
 */
links.Timeline.addEventListener = function (element, action, listener, useCapture) {
  if (element.addEventListener) {
    if (useCapture === undefined)
      useCapture = false;

    if (action === "mousewheel" && navigator.userAgent.indexOf("Firefox") >= 0) {
      action = "DOMMouseScroll";  // For Firefox
    }

    element.addEventListener(action, listener, useCapture);
  } else {
    element.attachEvent("on" + action, listener);  // IE browsers
  }
};

/**
 * Remove an event listener from an element
 * @param {DOM element}  element   An html dom element
 * @param {string}       action    The name of the event, for example "mousedown"
 * @param {function}     listener  The listener function
 * @param {boolean}      useCapture
 */
links.Timeline.removeEventListener = function(element, action, listener, useCapture) {
  if (element.removeEventListener) {
    // non-IE browsers
    if (useCapture === undefined)
      useCapture = false;

    if (action === "mousewheel" && navigator.userAgent.indexOf("Firefox") >= 0) {
      action = "DOMMouseScroll";  // For Firefox
    }

    element.removeEventListener(action, listener, useCapture);
  } else {
    // IE browsers
    element.detachEvent("on" + action, listener);
  }
};


/**
 * Get HTML element which is the target of the event
 * @param {MouseEvent} event
 * @return {HTML DOM} target element
 */
links.Timeline.getTarget = function (event) {
  // code from http://www.quirksmode.org/js/events_properties.html
  if (!event) {
    var event = window.event;
  }

  var target;

  if (event.target) {
    target = event.target;
  }
  else if (event.srcElement) {
    target = event.srcElement;
  }

  if (target.nodeType !== undefined && target.nodeType == 3) {
    // defeat Safari bug
    target = target.parentNode;
  }

  return target;
}

/**
 * Stop event propagation
 */
links.Timeline.stopPropagation = function (event) {
  if (!event)
    var event = window.event;

  if (event.stopPropagation) {
    event.stopPropagation();  // non-IE browsers
  }
  else {
    event.cancelBubble = true;  // IE browsers
  }
}


/**
 * Cancels the event if it is cancelable, without stopping further propagation of the event.
 */
links.Timeline.preventDefault = function (event) {
  if (!event)
    var event = window.event;

  if (event.preventDefault) {
    event.preventDefault();  // non-IE browsers
  }
  else {
    event.returnValue = false;  // IE browsers
  }
}


/**
 * Retrieve the absolute left value of a DOM element
 * @param {DOM element} elem    A dom element, for example a div
 * @return {number} left        The absolute left position of this element
 *                              in the browser page.
 */
links.Timeline.getAbsoluteLeft = function(elem)
{
  var left = 0;
  while( elem != null ) {
    left += elem.offsetLeft;
    left -= elem.scrollLeft;
    elem = elem.offsetParent;
  }
  if (!document.body.scrollLeft && window.pageXOffset) {
      // FF
      left -= window.pageXOffset;
  }
  return left;
}

/**
 * Retrieve the absolute top value of a DOM element
 * @param {DOM element} elem    A dom element, for example a div
 * @return {number} top        The absolute top position of this element
 *                              in the browser page.
 */
links.Timeline.getAbsoluteTop = function(elem)
{
  var top = 0;
  while( elem != null ) {
    top += elem.offsetTop;
    top -= elem.scrollTop;
    elem = elem.offsetParent;
  }
  if (!document.body.scrollTop && window.pageYOffset) {
      // FF
      top -= window.pageYOffset;
  }
  return top;
}

/**
 * Check if given object is a Javascript Array
 * @param {any type} obj
 * @return {Boolean} isArray    true if the given object is an array
 */
// See http://stackoverflow.com/questions/2943805/javascript-instanceof-typeof-in-gwt-jsni
links.Timeline.isArray = function (obj) {
  if (obj instanceof Array) {
    return true;
  }
  return (Object.prototype.toString.call(obj) === '[object Array]');
}

      var timeline;
      var data;

      // Called when the Visualization API is loaded.
config.macros.drawVisualization = {};
config.macros.drawVisualization.handler = function (place,macroName,params,wikifier,paramString,tiddler){
        // specify options
        var options = {
          'width':  '100%',
          'height': '500px',
          'editable': false,   // enable dragging and editing events
          'style': 'box',
          'showNavigation': true
        };

        // Instantiate our timeline object.
        timeline = new links.Timeline(place);

        function onRangeChanged(properties) {
          document.getElementById('info').innerHTML += 'rangechanged ' +
            properties.start + ' - ' + properties.end + '<br>';
        };

        // attach an event listener using the links events handler
        links.events.addListener(timeline, 'rangechanged', onRangeChanged);

        // Draw our timeline with the created data and options
//        Examples:
//        timeline.draw(data1, options);
//	  timeline.addItem({'start': new Date(2012,01,24), 'end': new Date(2012,2,24), 'content': '<div style="background-color:#00FFFF; border:0px solid green;padding:0px;">Possible visibile</div>',             'group': 'T-12-03' });
	params = paramString.parseParams("anon",null,true,false,false);
//	timeline.draw([], options);
	var title = getParam(params,"anon","");
	if(title == "" && tiddler instanceof Tiddler)
		title = tiddler.title;
	var sortby = getParam(params,"sortBy",false);
	var tagged = store.getTaggedTiddlers(title,sortby);
	var t;
	for(t=0; t<tagged.length; t++) {
		var StartDate = store.getTiddlerSlice(tagged[t].title,"StartDate") ;
		var SDDate = new Date(StartDate);
		var EndDate = store.getTiddlerSlice(tagged[t].title,"EndDate") ;
		var EDDate = new Date(EndDate);
		var BarColor = store.getTiddlerSlice(tagged[t].title,"Color") ;
		var BarText= store.getTiddlerSlice(tagged[t].title,"Text") ;
		var BarGroup= store.getTiddlerSlice(tagged[t].title,"Group") ;
		var CTcontent = '<div style="background-color:' + BarColor + '; border:0px solid ' + BarColor + '; padding:0px;">' + BarText + '</div>';

		if ((Date.parse(StartDate) || 0) > 0) {
			if ((Date.parse(EndDate) || 0) > 0)
				{
				timeline.addItem({'start': SDDate, 'end': EDDate , 'content': CTcontent, 'group': BarGroup});
				}
			else
				{
				timeline.addItem({'start': SDDate, 'content': CTcontent, 'group': BarGroup});
				}
		}
	}
	timeline.setVisibleChartRangeAuto();
}

//}}}
/***
|''Name:''|DataTiddlerPlugin|
|''Version:''|1.0.6 (2006-08-26)|
|''Source:''|http://tiddlywiki.abego-software.de/#DataTiddlerPlugin|
|''Author:''|UdoBorkowski (ub [at] abego-software [dot] de)|
|''Licence:''|[[BSD open source license]]|
|''TiddlyWiki:''|1.2.38+, 2.0|
|''Browser:''|Firefox 1.0.4+; InternetExplorer 6.0|
!Description
Enhance your tiddlers with structured data (such as strings, booleans, numbers, or even arrays and compound objects) that can be easily accessed and modified through named fields (in JavaScript code).

Such tiddler data can be used in various applications. E.g. you may create tables that collect data from various tiddlers.

''//Example: "Table with all December Expenses"//''
{{{
<<forEachTiddler
    where
        'tiddler.tags.contains("expense") && tiddler.data("month") == "Dec"'
    write
        '"|[["+tiddler.title+"]]|"+tiddler.data("descr")+"| "+tiddler.data("amount")+"|\n"'
>>
}}}
//(This assumes that expenses are stored in tiddlers tagged with "expense".)//
<<forEachTiddler
    where
        'tiddler.tags.contains("expense") && tiddler.data("month") == "Dec"'
    write
        '"|[["+tiddler.title+"]]|"+tiddler.data("descr")+"| "+tiddler.data("amount")+"|\n"'
>>
For other examples see DataTiddlerExamples.




''Access and Modify Tiddler Data''

You can "attach" data to every tiddler by assigning a JavaScript value (such as a string, boolean, number, or even arrays and compound objects) to named fields.

These values can be accessed and modified through the following Tiddler methods:
|!Method|!Example|!Description|
|{{{data(field)}}}|{{{t.data("age")}}}|Returns the value of the given data field of the tiddler. When no such field is defined or its value is undefined {{{undefined}}} is returned.|
|{{{data(field,defaultValue)}}}|{{{t.data("isVIP",false)}}}|Returns the value of the given data field of the tiddler. When no such field is defined or its value is undefined the defaultValue is returned.|
|{{{data()}}}|{{{t.data()}}}|Returns the data object of the tiddler, with a property for every field. The properties of the returned data object may only be read and not be modified. To modify the data use DataTiddler.setData(...) or the corresponding Tiddler method.|
|{{{setData(field,value)}}}|{{{t.setData("age",42)}}}|Sets the value of the given data field of the tiddler to the value. When the value is {{{undefined}}} the field is removed.|
|{{{setData(field,value,defaultValue)}}}|{{{t.setData("isVIP",flag,false)}}}|Sets the value of the given data field of the tiddler to the value. When the value is equal to the defaultValue no value is set (and the field is removed).|

Alternatively you may use the following functions to access and modify the data. In this case the tiddler argument is either a tiddler or the name of a tiddler.
|!Method|!Description|
|{{{DataTiddler.getData(tiddler,field)}}}|Returns the value of the given data field of the tiddler. When no such field is defined or its value is undefined {{{undefined}}} is returned.|
|{{{DataTiddler.getData(tiddler,field,defaultValue)}}}|Returns the value of the given data field of the tiddler. When no such field is defined or its value is undefined the defaultValue is returned.|
|{{{DataTiddler.getDataObject(tiddler)}}}|Returns the data object of the tiddler, with a property for every field. The properties of the returned data object may only be read and not be modified. To modify the data use DataTiddler.setData(...) or the corresponding Tiddler method.|
|{{{DataTiddler.setData(tiddler,field,value)}}}|Sets the value of the given data field of the tiddler to the value. When the value is {{{undefined}}} the field is removed.|
|{{{DataTiddler.setData(tiddler,field,value,defaultValue)}}}|Sets the value of the given data field of the tiddler to the value. When the value is equal to the defaultValue no value is set (and the field is removed).|
//(For details on the various functions see the detailed comments in the source code.)//


''Data Representation in a Tiddler''

The data of a tiddler is stored as plain text in the tiddler's content/text, inside a "data" section that is framed by a {{{<data>...</data>}}} block. Inside the data section the information is stored in the [[JSON format|http://www.crockford.com/JSON/index.html]].

//''Data Section Example:''//
{{{
<data>{"isVIP":true,"user":"John Brown","age":34}</data>
}}}

The data section is not displayed when viewing the tiddler (see also "The showData Macro").

Beside the data section a tiddler may have all kind of other content.

Typically you will not access the data section text directly but use the methods given above. Nevertheless you may retrieve the text of the data section's content through the {{{DataTiddler.getDataText(tiddler)}}} function.


''Saving Changes''

The "setData" methods respect the "ForceMinorUpdate" and "AutoSave" configuration values. I.e. when "ForceMinorUpdate" is true changing a value using setData will not affect the "modifier" and "modified" attributes. With "AutoSave" set to true every setData will directly save the changes after a setData.


''Notifications''

No notifications are sent when a tiddler's data value is changed through the "setData" methods.

''Escape Data Section''
In case that you want to use the text {{{<data>}}} or {{{</data>}}} in a tiddler text you must prefix the text with a tilde ('~'). Otherwise it may be wrongly considered as the data section. The tiddler text {{{~<data>}}} is displayed as {{{<data>}}}.


''The showData Macro''

By default the data of a tiddler (that is stored in the {{{<data>...</data>}}} section of the tiddler) is not displayed. If you want to display this data you may used the {{{<<showData ...>>}}} macro:

''Syntax:''
|>|{{{<<}}}''showData '' [''JSON''] [//tiddlerName//] {{{>>}}}|
|''JSON''|By default the data is rendered as a table with a "Name" and "Value" column. When defining ''JSON'' the data is rendered in JSON format|
|//tiddlerName//|Defines the tiddler holding the data to be displayed. When no tiddler is given the tiddler containing the showData macro is used. When the tiddler name contains spaces you must quote the name (or use the {{{[[...]]}}} syntax.)|
|>|~~Syntax formatting: Keywords in ''bold'', optional parts in [...]. 'or' means that exactly one of the two alternatives must exist.~~|


!Revision history
* v1.0.6 (2006-08-26)
** Removed misleading comment
* v1.0.5 (2006-02-27) (Internal Release Only)
** Internal
*** Make "JSLint" conform
* v1.0.4 (2006-02-05)
** Bugfix: showData fails in TiddlyWiki 2.0
* v1.0.3 (2006-01-06)
** Support TiddlyWiki 2.0
* v1.0.2 (2005-12-22)
** Enhancements:
*** Handle texts "<data>" or "</data>" more robust when used in a tiddler text or as a field value.
*** Improved (JSON) error messages.
** Bugs fixed:
*** References are not updated when using the DataTiddler.
*** Changes to compound objects are not always saved.
*** "~</data>" is not rendered correctly (expected "</data>")
* v1.0.1 (2005-12-13)
** Features:
*** The showData macro supports an optional "tiddlername" argument to specify the tiddler containing the data to be displayed
** Bugs fixed:
*** A script immediately following a data section is deleted when the data is changed. (Thanks to GeoffS for reporting.)
* v1.0.0 (2005-12-12)
** initial version

!Code
***/
//{{{
//============================================================================
//============================================================================
//                           DataTiddlerPlugin
//============================================================================
//============================================================================

// Ensure that the DataTiddler Plugin is only installed once.
//
if (!version.extensions.DataTiddlerPlugin) {



version.extensions.DataTiddlerPlugin = {
    major: 1, minor: 0, revision: 6,
    date: new Date(2006, 7, 26),
    type: 'plugin',
    source: "http://tiddlywiki.abego-software.de/#DataTiddlerPlugin"
};

// For backward compatibility with v1.2.x
//
if (!window.story) window.story=window;
if (!TiddlyWiki.prototype.getTiddler) {
	TiddlyWiki.prototype.getTiddler = function(title) {
		var t = this.tiddlers[title];
		return (t !== undefined && t instanceof Tiddler) ? t : null;
	};
}

//============================================================================
// DataTiddler Class
//============================================================================

// ---------------------------------------------------------------------------
// Configurations and constants
// ---------------------------------------------------------------------------

function DataTiddler() {
}

DataTiddler = {
    // Function to stringify a JavaScript value, producing the text for the data section content.
    // (Must match the implementation of DataTiddler.parse.)
    //
    stringify : null,


    // Function to parse the text for the data section content, producing a JavaScript value.
    // (Must match the implementation of DataTiddler.stringify.)
    //
    parse : null
};

// Ensure access for IE
window.DataTiddler = DataTiddler;

// ---------------------------------------------------------------------------
// Data Accessor and Mutator
// ---------------------------------------------------------------------------


// Returns the value of the given data field of the tiddler.
// When no such field is defined or its value is undefined
// the defaultValue is returned.
//
// @param tiddler either a tiddler name or a tiddler
//
DataTiddler.getData = function(tiddler, field, defaultValue) {
    var t = (typeof tiddler == "string") ? store.getTiddler(tiddler) : tiddler;
    if (!(t instanceof Tiddler)) {
        throw "Tiddler expected. Got "+tiddler;
    }

    return DataTiddler.getTiddlerDataValue(t, field, defaultValue);
};


// Sets the value of the given data field of the tiddler to
// the value. When the value is equal to the defaultValue
// no value is set (and the field is removed)
//
// Changing data of a tiddler will not trigger notifications.
//
// @param tiddler either a tiddler name or a tiddler
//
DataTiddler.setData = function(tiddler, field, value, defaultValue) {
    var t = (typeof tiddler == "string") ? store.getTiddler(tiddler) : tiddler;
    if (!(t instanceof Tiddler)) {
        throw "Tiddler expected. Got "+tiddler+ "("+t+")";
    }

    DataTiddler.setTiddlerDataValue(t, field, value, defaultValue);
};


// Returns the data object of the tiddler, with a property for every field.
//
// The properties of the returned data object may only be read and
// not be modified. To modify the data use DataTiddler.setData(...)
// or the corresponding Tiddler method.
//
// If no data section is defined a new (empty) object is returned.
//
// @param tiddler either a tiddler name or a Tiddler
//
DataTiddler.getDataObject = function(tiddler) {
    var t = (typeof tiddler == "string") ? store.getTiddler(tiddler) : tiddler;
    if (!(t instanceof Tiddler)) {
        throw "Tiddler expected. Got "+tiddler;
    }

    return DataTiddler.getTiddlerDataObject(t);
};

// Returns the text of the content of the data section of the tiddler.
//
// When no data section is defined for the tiddler null is returned
//
// @param tiddler either a tiddler name or a Tiddler
// @return [may be null]
//
DataTiddler.getDataText = function(tiddler) {
    var t = (typeof tiddler == "string") ? store.getTiddler(tiddler) : tiddler;
    if (!(t instanceof Tiddler)) {
        throw "Tiddler expected. Got "+tiddler;
    }

    return DataTiddler.readDataSectionText(t);
};


// ---------------------------------------------------------------------------
// Internal helper methods (must not be used by code from outside this plugin)
// ---------------------------------------------------------------------------

// Internal.
//
// The original JSONError is not very user friendly,
// especially it does not define a toString() method
// Therefore we extend it here.
//
DataTiddler.extendJSONError = function(ex) {
	if (ex.name == 'JSONError') {
        ex.toString = function() {
			return ex.name + ": "+ex.message+" ("+ex.text+")";
		};
	}
	return ex;
};

// Internal.
//
// @param t a Tiddler
//
DataTiddler.getTiddlerDataObject = function(t) {
    if (t.dataObject === undefined) {
        var data = DataTiddler.readData(t);
        t.dataObject = (data) ? data : {};
    }

    return t.dataObject;
};


// Internal.
//
// @param tiddler a Tiddler
//
DataTiddler.getTiddlerDataValue = function(tiddler, field, defaultValue) {
    var value = DataTiddler.getTiddlerDataObject(tiddler)[field];
    return (value === undefined) ? defaultValue : value;
};


// Internal.
//
// @param tiddler a Tiddler
//
DataTiddler.setTiddlerDataValue = function(tiddler, field, value, defaultValue) {
    var data = DataTiddler.getTiddlerDataObject(tiddler);
    var oldValue = data[field];

    if (value == defaultValue) {
        if (oldValue !== undefined) {
            delete data[field];
            DataTiddler.save(tiddler);
        }
        return;
    }
    data[field] = value;
    DataTiddler.save(tiddler);
};

// Internal.
//
// Reads the data section from the tiddler's content and returns its text
// (as a String).
//
// Returns null when no data is defined.
//
// @param tiddler a Tiddler
// @return [may be null]
//
DataTiddler.readDataSectionText = function(tiddler) {
    var matches = DataTiddler.getDataTiddlerMatches(tiddler);
    if (matches === null || !matches[2]) {
        return null;
    }
    return matches[2];
};

// Internal.
//
// Reads the data section from the tiddler's content and returns it
// (as an internalized object).
//
// Returns null when no data is defined.
//
// @param tiddler a Tiddler
// @return [may be null]
//
DataTiddler.readData = function(tiddler) {
    var text = DataTiddler.readDataSectionText(tiddler);
	try {
	    return text ? DataTiddler.parse(text) : null;
	} catch(ex) {
		throw DataTiddler.extendJSONError(ex);
	}
};

// Internal.
//
// Returns the serialized text of the data of the given tiddler, as it
// should be stored in the data section.
//
// @param tiddler a Tiddler
//
DataTiddler.getDataTextOfTiddler = function(tiddler) {
    var data = DataTiddler.getTiddlerDataObject(tiddler);
    return DataTiddler.stringify(data);
};


// Internal.
//
DataTiddler.indexOfNonEscapedText = function(s, subString, startIndex) {
	var index = s.indexOf(subString, startIndex);
	while ((index > 0) && (s[index-1] == '~')) {
		index = s.indexOf(subString, index+1);
	}
	return index;
};

// Internal.
//
DataTiddler.getDataSectionInfo = function(text) {
	// Special care must be taken to handle "<data>" and "</data>" texts inside
	// a data section.
	// Also take care not to use an escaped <data> (i.e. "~<data>") as the start
	// of a data section. (Same for </data>)

    // NOTE: we are explicitly searching for a data section that contains a JSON
    // string, i.e. framed with braces. This way we are little bit more robust in
    // case the tiddler contains unescaped texts "<data>" or "</data>". This must
    // be changed when using a different stringifier.

	var startTagText = "<data>{";
	var endTagText = "}</data>";

	var startPos = 0;

	// Find the first not escaped "<data>".
	var startDataTagIndex = DataTiddler.indexOfNonEscapedText(text, startTagText, 0);
	if (startDataTagIndex < 0) {
		return null;
	}

	// Find the *last* not escaped "</data>".
	var endDataTagIndex = text.indexOf(endTagText, startDataTagIndex);
	if (endDataTagIndex < 0) {
		return null;
	}
	var nextEndDataTagIndex;
	while ((nextEndDataTagIndex = text.indexOf(endTagText, endDataTagIndex+1)) >= 0) {
		endDataTagIndex = nextEndDataTagIndex;
	}

	return {
		prefixEnd: startDataTagIndex,
		dataStart: startDataTagIndex+(startTagText.length)-1,
		dataEnd: endDataTagIndex,
		suffixStart: endDataTagIndex+(endTagText.length)
	};
};

// Internal.
//
// Returns the "matches" of a content of a DataTiddler on the
// "data" regular expression. Return null when no data is defined
// in the tiddler content.
//
// Group 1: text before data section (prefix)
// Group 2: content of data section
// Group 3: text behind data section (suffix)
//
// @param tiddler a Tiddler
// @return [may be null] null when the tiddler contains no data section, otherwise see above.
//
DataTiddler.getDataTiddlerMatches = function(tiddler) {
	var text = tiddler.text;
	var info = DataTiddler.getDataSectionInfo(text);
	if (!info) {
		return null;
	}

	var prefix = text.substr(0,info.prefixEnd);
	var data = text.substr(info.dataStart, info.dataEnd-info.dataStart+1);
	var suffix = text.substr(info.suffixStart);

	return [text, prefix, data, suffix];
};


// Internal.
//
// Saves the data in a <data> block of the given tiddler (as a minor change).
//
// The "chkAutoSave" and "chkForceMinorUpdate" options are respected.
// I.e. the TiddlyWiki *file* is only saved when AutoSave is on.
//
// Notifications are not send.
//
// This method should only be called when the data really has changed.
//
// @param tiddler
//             the tiddler to be saved.
//
DataTiddler.save = function(tiddler) {

    var matches = DataTiddler.getDataTiddlerMatches(tiddler);

    var prefix;
    var suffix;
    if (matches === null) {
        prefix = tiddler.text;
        suffix = "";
    } else {
        prefix = matches[1];
        suffix = matches[3];
    }

    var dataText = DataTiddler.getDataTextOfTiddler(tiddler);
    var newText =
            (dataText !== null)
                ? prefix + "<data>" + dataText + "</data>" + suffix
                : prefix + suffix;
    if (newText != tiddler.text) {
        // make the change in the tiddlers text

        // ... see DataTiddler.MyTiddlerChangedFunction
        tiddler.isDataTiddlerChange = true;

        // ... do the action change
        tiddler.set(
                tiddler.title,
                newText,
                config.options.txtUserName,
                config.options.chkForceMinorUpdate? undefined : new Date(),
                tiddler.tags);

        // ... see DataTiddler.MyTiddlerChangedFunction
        delete tiddler.isDataTiddlerChange;

        // Mark the store as dirty.
        store.dirty = true;

        // AutoSave if option is selected
        if(config.options.chkAutoSave) {
           saveChanges();
        }
    }
};

// Internal.
//
DataTiddler.MyTiddlerChangedFunction = function() {
    // Remove the data object from the tiddler when the tiddler is changed
    // by code other than DataTiddler code.
    //
    // This is necessary since the data object is just a "cached version"
    // of the data defined in the data section of the tiddler and the
    // "external" change may have changed the content of the data section.
    // Thus we are not sure if the data object reflects the data section
    // contents.
    //
    // By deleting the data object we ensure that the data object is
    // reconstructed the next time it is needed, with the data defined by
    // the data section in the tiddler's text.

    // To indicate that a change is a "DataTiddler change" a temporary
    // property "isDataTiddlerChange" is added to the tiddler.
    if (this.dataObject && !this.isDataTiddlerChange) {
        delete this.dataObject;
    }

    // call the original code.
	DataTiddler.originalTiddlerChangedFunction.apply(this, arguments);
};


//============================================================================
// Formatters
//============================================================================

// This formatter ensures that "~<data>" is rendered as "<data>". This is used to
// escape the "<data>" of a data section, just in case someone really wants to use
// "<data>" as a text in a tiddler and not start a data section.
//
// Same for </data>.
//
config.formatters.push( {
    name: "data-escape",
    match: "~<\\/?data>",

    handler: function(w) {
            w.outputText(w.output,w.matchStart + 1,w.nextMatch);
    }
} );


// This formatter ensures that <data>...</data> sections are not rendered.
//
config.formatters.push( {
    name: "data",
    match: "<data>",

    handler: function(w) {
		var info = DataTiddler.getDataSectionInfo(w.source);
		if (info && info.prefixEnd == w.matchStart) {
            w.nextMatch = info.suffixStart;
		} else {
			w.outputText(w.output,w.matchStart,w.nextMatch);
		}
    }
} );


//============================================================================
// Tiddler Class Extension
//============================================================================

// "Hijack" the changed method ---------------------------------------------------

DataTiddler.originalTiddlerChangedFunction = Tiddler.prototype.changed;
Tiddler.prototype.changed = DataTiddler.MyTiddlerChangedFunction;

// Define accessor methods -------------------------------------------------------

// Returns the value of the given data field of the tiddler. When no such field
// is defined or its value is undefined the defaultValue is returned.
//
// When field is undefined (or null) the data object is returned. (See
// DataTiddler.getDataObject.)
//
// @param field [may be null, undefined]
// @param defaultValue [may be null, undefined]
// @return [may be null, undefined]
//
Tiddler.prototype.data = function(field, defaultValue) {
    return (field)
         ? DataTiddler.getTiddlerDataValue(this, field, defaultValue)
         : DataTiddler.getTiddlerDataObject(this);
};

// Sets the value of the given data field of the tiddler to the value. When the
// value is equal to the defaultValue no value is set (and the field is removed).
//
// @param value [may be null, undefined]
// @param defaultValue [may be null, undefined]
//
Tiddler.prototype.setData = function(field, value, defaultValue) {
    DataTiddler.setTiddlerDataValue(this, field, value, defaultValue);
};


//============================================================================
// showData Macro
//============================================================================

config.macros.showData = {
     // Standard Properties
     label: "showData",
     prompt: "Display the values stored in the data section of the tiddler"
};

config.macros.showData.handler = function(place,macroName,params) {
    // --- Parsing ------------------------------------------

    var i = 0; // index running over the params
    // Parse the optional "JSON"
    var showInJSONFormat = false;
    if ((i < params.length) && params[i] == "JSON") {
        i++;
        showInJSONFormat = true;
    }

    var tiddlerName = story.findContainingTiddler(place).id.substr(7);
    if (i < params.length) {
        tiddlerName = params[i];
        i++;
    }

    // --- Processing ------------------------------------------
    try {
        if (showInJSONFormat) {
            this.renderDataInJSONFormat(place, tiddlerName);
        } else {
            this.renderDataAsTable(place, tiddlerName);
        }
    } catch (e) {
        this.createErrorElement(place, e);
    }
};

config.macros.showData.renderDataInJSONFormat = function(place,tiddlerName) {
    var text = DataTiddler.getDataText(tiddlerName);
    if (text) {
        createTiddlyElement(place,"pre",null,null,text);
    }
};

config.macros.showData.renderDataAsTable = function(place,tiddlerName) {
    var text = "|!Name|!Value|\n";
    var data = DataTiddler.getDataObject(tiddlerName);
    if (data) {
        for (var i in data) {
            var value = data[i];
            text += "|"+i+"|"+DataTiddler.stringify(value)+"|\n";
        }
    }

    wikify(text, place);
};


// Internal.
//
// Creates an element that holds an error message
//
config.macros.showData.createErrorElement = function(place, exception) {
    var message = (exception.description) ? exception.description : exception.toString();
    return createTiddlyElement(place,"span",null,"showDataError","<<showData ...>>: "+message);
};

// ---------------------------------------------------------------------------
// Stylesheet Extensions (may be overridden by local StyleSheet)
// ---------------------------------------------------------------------------
//
setStylesheet(
    ".showDataError{color: #ffffff;background-color: #880000;}",
    "showData");


} // of "install only once"
// Used Globals (for JSLint) ==============

// ... TiddlyWiki Core
/*global 	createTiddlyElement, saveChanges, store, story, wikify */
// ... DataTiddler
/*global 	DataTiddler */
// ... JSON
/*global 	JSON */


/***
!JSON Code, used to serialize the data
***/
/*
Copyright (c) 2005 JSON.org

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The Software shall be used for Good, not Evil.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/

/*
    The global object JSON contains two methods.

    JSON.stringify(value) takes a JavaScript value and produces a JSON text.
    The value must not be cyclical.

    JSON.parse(text) takes a JSON text and produces a JavaScript value. It will
    throw a 'JSONError' exception if there is an error.
*/
var JSON = {
    copyright: '(c)2005 JSON.org',
    license: 'http://www.crockford.com/JSON/license.html',
/*
    Stringify a JavaScript value, producing a JSON text.
*/
    stringify: function (v) {
        var a = [];

/*
    Emit a string.
*/
        function e(s) {
            a[a.length] = s;
        }

/*
    Convert a value.
*/
        function g(x) {
            var c, i, l, v;

            switch (typeof x) {
            case 'object':
                if (x) {
                    if (x instanceof Array) {
                        e('[');
                        l = a.length;
                        for (i = 0; i < x.length; i += 1) {
                            v = x[i];
                            if (typeof v != 'undefined' &&
                                    typeof v != 'function') {
                                if (l < a.length) {
                                    e(',');
                                }
                                g(v);
                            }
                        }
                        e(']');
                        return;
                    } else if (typeof x.toString != 'undefined') {
                        e('{');
                        l = a.length;
                        for (i in x) {
                            v = x[i];
                            if (x.hasOwnProperty(i) &&
                                    typeof v != 'undefined' &&
                                    typeof v != 'function') {
                                if (l < a.length) {
                                    e(',');
                                }
                                g(i);
                                e(':');
                                g(v);
                            }
                        }
                        return e('}');
                    }
                }
                e('null');
                return;
            case 'number':
                e(isFinite(x) ? +x : 'null');
                return;
            case 'string':
                l = x.length;
                e('"');
                for (i = 0; i < l; i += 1) {
                    c = x.charAt(i);
                    if (c >= ' ') {
                        if (c == '\\' || c == '"') {
                            e('\\');
                        }
                        e(c);
                    } else {
                        switch (c) {
                            case '\b':
                                e('\\b');
                                break;
                            case '\f':
                                e('\\f');
                                break;
                            case '\n':
                                e('\\n');
                                break;
                            case '\r':
                                e('\\r');
                                break;
                            case '\t':
                                e('\\t');
                                break;
                            default:
                                c = c.charCodeAt();
                                e('\\u00' + Math.floor(c / 16).toString(16) +
                                    (c % 16).toString(16));
                        }
                    }
                }
                e('"');
                return;
            case 'boolean':
                e(String(x));
                return;
            default:
                e('null');
                return;
            }
        }
        g(v);
        return a.join('');
    },
/*
    Parse a JSON text, producing a JavaScript value.
*/
    parse: function (text) {
        var p = /^\s*(([,:{}\[\]])|"(\\.|[^\x00-\x1f"\\])*"|-?\d+(\.\d*)?([eE][+-]?\d+)?|true|false|null)\s*/,
            token,
            operator;

        function error(m, t) {
            throw {
                name: 'JSONError',
                message: m,
                text: t || operator || token
            };
        }

        function next(b) {
            if (b && b != operator) {
                error("Expected '" + b + "'");
            }
            if (text) {
                var t = p.exec(text);
                if (t) {
                    if (t[2]) {
                        token = null;
                        operator = t[2];
                    } else {
                        operator = null;
                        try {
                            token = eval(t[1]);
                        } catch (e) {
                            error("Bad token", t[1]);
                        }
                    }
                    text = text.substring(t[0].length);
                } else {
                    error("Unrecognized token", text);
                }
            } else {
                token = operator = undefined;
            }
        }


        function val() {
            var k, o;
            switch (operator) {
            case '{':
                next('{');
                o = {};
                if (operator != '}') {
                    for (;;) {
                        if (operator || typeof token != 'string') {
                            error("Missing key");
                        }
                        k = token;
                        next();
                        next(':');
                        o[k] = val();
                        if (operator != ',') {
                            break;
                        }
                        next(',');
                    }
                }
                next('}');
                return o;
            case '[':
                next('[');
                o = [];
                if (operator != ']') {
                    for (;;) {
                        o.push(val());
                        if (operator != ',') {
                            break;
                        }
                        next(',');
                    }
                }
                next(']');
                return o;
            default:
                if (operator !== null) {
                    error("Missing value");
                }
                k = token;
                next();
                return k;
            }
        }
        next();
        return val();
    }
};

/***
!Setup the data serialization
***/

DataTiddler.format = "JSON";
DataTiddler.stringify = JSON.stringify;
DataTiddler.parse = JSON.parse;

//}}}
StartDate:2012-01-24
EndDate:2012-09-29
Color:Cyan
Text:Possible Start
Group:Group 1
StartDate:2012-09-29
EndDate:2013-02-28
Color:Orange
Text:Possible End
Group:Group 1
StartDate:2012-05-14
EndDate:2012-07-14
Color:Yellow
Text:Single Bar
Group:Group 2
StartDate:2012-06-14
EndDate:
Color:
Text:Single<br>Box
Group:Group 3
StartDate:2012-08-01
EndDate:
Color:SkyBlue; width:60px; height:99px
Text:ABC<br><img src="http://png-1.findicons.com/files//icons/2083/go_green_web/64/mobile_phone.png" style="width:32px; height:32px;">
Group:
ContractNumber:ABC
Name:Contract 1
StartDate:2012-01-01
MiddleDate:2012-07-01
EndDate:2012-12-31
ContractNumber:XYZ
Name:Contract 2
StartDate:2012-02-01
MiddleDate:2012-03-01
EndDate:2012-04-01
/***
|''Name:''|ForEachTiddlerPlugin|
|''Version:''|1.0.8 (2007-04-12)|
|''Source:''|http://tiddlywiki.abego-software.de/#ForEachTiddlerPlugin|
|''Author:''|UdoBorkowski (ub [at] abego-software [dot] de)|
|''Licence:''|[[BSD open source license (abego Software)|http://www.abego-software.de/legal/apl-v10.html]]|
|''Copyright:''|&copy; 2005-2007 [[abego Software|http://www.abego-software.de]]|
|''TiddlyWiki:''|1.2.38+, 2.0|
|''Browser:''|Firefox 1.0.4+; Firefox 1.5; InternetExplorer 6.0|
!Description

Create customizable lists, tables etc. for your selections of tiddlers. Specify the tiddlers to include and their order through a powerful language.

''Syntax:''
|>|{{{<<}}}''forEachTiddler'' [''in'' //tiddlyWikiPath//] [''where'' //whereCondition//] [''sortBy'' //sortExpression// [''ascending'' //or// ''descending'']] [''script'' //scriptText//] [//action// [//actionParameters//]]{{{>>}}}|
|//tiddlyWikiPath//|The filepath to the TiddlyWiki the macro should work on. When missing the current TiddlyWiki is used.|
|//whereCondition//|(quoted) JavaScript boolean expression. May refer to the build-in variables {{{tiddler}}} and  {{{context}}}.|
|//sortExpression//|(quoted) JavaScript expression returning "comparable" objects (using '{{{<}}}','{{{>}}}','{{{==}}}'. May refer to the build-in variables {{{tiddler}}} and  {{{context}}}.|
|//scriptText//|(quoted) JavaScript text. Typically defines JavaScript functions that are called by the various JavaScript expressions (whereClause, sortClause, action arguments,...)|
|//action//|The action that should be performed on every selected tiddler, in the given order. By default the actions [[addToList|AddToListAction]] and [[write|WriteAction]] are supported. When no action is specified [[addToList|AddToListAction]]  is used.|
|//actionParameters//|(action specific) parameters the action may refer while processing the tiddlers (see action descriptions for details). <<tiddler [[JavaScript in actionParameters]]>>|
|>|~~Syntax formatting: Keywords in ''bold'', optional parts in [...]. 'or' means that exactly one of the two alternatives must exist.~~|

See details see [[ForEachTiddlerMacro]] and [[ForEachTiddlerExamples]].

!Revision history
* v1.0.8 (2007-04-12)
** Adapted to latest TiddlyWiki 2.2 Beta importTiddlyWiki API (introduced with changeset 2004). TiddlyWiki 2.2 Beta builds prior to changeset 2004 are no longer supported (but TiddlyWiki 2.1 and earlier, of cause)
* v1.0.7 (2007-03-28)
** Also support "pre" formatted TiddlyWikis (introduced with TW 2.2) (when using "in" clause to work on external tiddlers)
* v1.0.6 (2006-09-16)
** Context provides "viewerTiddler", i.e. the tiddler used to view the macro. Most times this is equal to the "inTiddler", but when using the "tiddler" macro both may be different.
** Support "begin", "end" and "none" expressions in "write" action
* v1.0.5 (2006-02-05)
** Pass tiddler containing the macro with wikify, context object also holds reference to tiddler containing the macro ("inTiddler"). Thanks to SimonBaird.
** Support Firefox 1.5.0.1
** Internal
*** Make "JSLint" conform
*** "Only install once"
* v1.0.4 (2006-01-06)
** Support TiddlyWiki 2.0
* v1.0.3 (2005-12-22)
** Features:
*** Write output to a file supports multi-byte environments (Thanks to Bram Chen)
*** Provide API to access the forEachTiddler functionality directly through JavaScript (see getTiddlers and performMacro)
** Enhancements:
*** Improved error messages on InternetExplorer.
* v1.0.2 (2005-12-10)
** Features:
*** context object also holds reference to store (TiddlyWiki)
** Fixed Bugs:
*** ForEachTiddler 1.0.1 has broken support on win32 Opera 8.51 (Thanks to BrunoSabin for reporting)
* v1.0.1 (2005-12-08)
** Features:
*** Access tiddlers stored in separated TiddlyWikis through the "in" option. I.e. you are no longer limited to only work on the "current TiddlyWiki".
*** Write output to an external file using the "toFile" option of the "write" action. With this option you may write your customized tiddler exports.
*** Use the "script" section to define "helper" JavaScript functions etc. to be used in the various JavaScript expressions (whereClause, sortClause, action arguments,...).
*** Access and store context information for the current forEachTiddler invocation (through the build-in "context" object) .
*** Improved script evaluation (for where/sort clause and write scripts).
* v1.0.0 (2005-11-20)
** initial version

!Code
***/
//{{{


//============================================================================
//============================================================================
//		   ForEachTiddlerPlugin
//============================================================================
//============================================================================

// Only install once
if (!version.extensions.ForEachTiddlerPlugin) {

if (!window.abego) window.abego = {};

version.extensions.ForEachTiddlerPlugin = {
	major: 1, minor: 0, revision: 8,
	date: new Date(2007,3,12),
	source: "http://tiddlywiki.abego-software.de/#ForEachTiddlerPlugin",
	licence: "[[BSD open source license (abego Software)|http://www.abego-software.de/legal/apl-v10.html]]",
	copyright: "Copyright (c) abego Software GmbH, 2005-2007 (www.abego-software.de)"
};

// For backward compatibility with TW 1.2.x
//
if (!TiddlyWiki.prototype.forEachTiddler) {
	TiddlyWiki.prototype.forEachTiddler = function(callback) {
		for(var t in this.tiddlers) {
			callback.call(this,t,this.tiddlers[t]);
		}
	};
}

//============================================================================
// forEachTiddler Macro
//============================================================================

version.extensions.forEachTiddler = {
	major: 1, minor: 0, revision: 8, date: new Date(2007,3,12), provider: "http://tiddlywiki.abego-software.de"};

// ---------------------------------------------------------------------------
// Configurations and constants
// ---------------------------------------------------------------------------

config.macros.forEachTiddler = {
	 // Standard Properties
	 label: "forEachTiddler",
	 prompt: "Perform actions on a (sorted) selection of tiddlers",

	 // actions
	 actions: {
		 addToList: {},
		 write: {}
	 }
};

// ---------------------------------------------------------------------------
//  The forEachTiddler Macro Handler
// ---------------------------------------------------------------------------

config.macros.forEachTiddler.getContainingTiddler = function(e) {
	while(e && !hasClass(e,"tiddler"))
		e = e.parentNode;
	var title = e ? e.getAttribute("tiddler") : null;
	return title ? store.getTiddler(title) : null;
};

config.macros.forEachTiddler.handler = function(place,macroName,params,wikifier,paramString,tiddler) {
	// config.macros.forEachTiddler.traceMacroCall(place,macroName,params,wikifier,paramString,tiddler);

	if (!tiddler) tiddler = config.macros.forEachTiddler.getContainingTiddler(place);
	// --- Parsing ------------------------------------------

	var i = 0; // index running over the params
	// Parse the "in" clause
	var tiddlyWikiPath = undefined;
	if ((i < params.length) && params[i] == "in") {
		i++;
		if (i >= params.length) {
			this.handleError(place, "TiddlyWiki path expected behind 'in'.");
			return;
		}
		tiddlyWikiPath = this.paramEncode((i < params.length) ? params[i] : "");
		i++;
	}

	// Parse the where clause
	var whereClause ="true";
	if ((i < params.length) && params[i] == "where") {
		i++;
		whereClause = this.paramEncode((i < params.length) ? params[i] : "");
		i++;
	}

	// Parse the sort stuff
	var sortClause = null;
	var sortAscending = true;
	if ((i < params.length) && params[i] == "sortBy") {
		i++;
		if (i >= params.length) {
			this.handleError(place, "sortClause missing behind 'sortBy'.");
			return;
		}
		sortClause = this.paramEncode(params[i]);
		i++;

		if ((i < params.length) && (params[i] == "ascending" || params[i] == "descending")) {
			 sortAscending = params[i] == "ascending";
			 i++;
		}
	}

	// Parse the script
	var scriptText = null;
	if ((i < params.length) && params[i] == "script") {
		i++;
		scriptText = this.paramEncode((i < params.length) ? params[i] : "");
		i++;
	}

	// Parse the action.
	// When we are already at the end use the default action
	var actionName = "addToList";
	if (i < params.length) {
	   if (!config.macros.forEachTiddler.actions[params[i]]) {
			this.handleError(place, "Unknown action '"+params[i]+"'.");
			return;
		} else {
			actionName = params[i];
			i++;
		}
	}

	// Get the action parameter
	// (the parsing is done inside the individual action implementation.)
	var actionParameter = params.slice(i);


	// --- Processing ------------------------------------------
	try {
		this.performMacro({
				place: place,
				inTiddler: tiddler,
				whereClause: whereClause,
				sortClause: sortClause,
				sortAscending: sortAscending,
				actionName: actionName,
				actionParameter: actionParameter,
				scriptText: scriptText,
				tiddlyWikiPath: tiddlyWikiPath});

	} catch (e) {
		this.handleError(place, e);
	}
};

// Returns an object with properties "tiddlers" and "context".
// tiddlers holds the (sorted) tiddlers selected by the parameter,
// context the context of the execution of the macro.
//
// The action is not yet performed.
//
// @parameter see performMacro
//
config.macros.forEachTiddler.getTiddlersAndContext = function(parameter) {

	var context = config.macros.forEachTiddler.createContext(parameter.place, parameter.whereClause, parameter.sortClause, parameter.sortAscending, parameter.actionName, parameter.actionParameter, parameter.scriptText, parameter.tiddlyWikiPath, parameter.inTiddler);

	var tiddlyWiki = parameter.tiddlyWikiPath ? this.loadTiddlyWiki(parameter.tiddlyWikiPath) : store;
	context["tiddlyWiki"] = tiddlyWiki;

	// Get the tiddlers, as defined by the whereClause
	var tiddlers = this.findTiddlers(parameter.whereClause, context, tiddlyWiki);
	context["tiddlers"] = tiddlers;

	// Sort the tiddlers, when sorting is required.
	if (parameter.sortClause) {
		this.sortTiddlers(tiddlers, parameter.sortClause, parameter.sortAscending, context);
	}

	return {tiddlers: tiddlers, context: context};
};

// Returns the (sorted) tiddlers selected by the parameter.
//
// The action is not yet performed.
//
// @parameter see performMacro
//
config.macros.forEachTiddler.getTiddlers = function(parameter) {
	return this.getTiddlersAndContext(parameter).tiddlers;
};

// Performs the macros with the given parameter.
//
// @param parameter holds the parameter of the macro as separate properties.
//				  The following properties are supported:
//
//						place
//						whereClause
//						sortClause
//						sortAscending
//						actionName
//						actionParameter
//						scriptText
//						tiddlyWikiPath
//
//					All properties are optional.
//					For most actions the place property must be defined.
//
config.macros.forEachTiddler.performMacro = function(parameter) {
	var tiddlersAndContext = this.getTiddlersAndContext(parameter);

	// Perform the action
	var actionName = parameter.actionName ? parameter.actionName : "addToList";
	var action = config.macros.forEachTiddler.actions[actionName];
	if (!action) {
		this.handleError(parameter.place, "Unknown action '"+actionName+"'.");
		return;
	}

	var actionHandler = action.handler;
	actionHandler(parameter.place, tiddlersAndContext.tiddlers, parameter.actionParameter, tiddlersAndContext.context);
};

// ---------------------------------------------------------------------------
//  The actions
// ---------------------------------------------------------------------------

// Internal.
//
// --- The addToList Action -----------------------------------------------
//
config.macros.forEachTiddler.actions.addToList.handler = function(place, tiddlers, parameter, context) {
	// Parse the parameter
	var p = 0;

	// Check for extra parameters
	if (parameter.length > p) {
		config.macros.forEachTiddler.createExtraParameterErrorElement(place, "addToList", parameter, p);
		return;
	}

	// Perform the action.
	var list = document.createElement("ul");
	place.appendChild(list);
	for (var i = 0; i < tiddlers.length; i++) {
		var tiddler = tiddlers[i];
		var listItem = document.createElement("li");
		list.appendChild(listItem);
		createTiddlyLink(listItem, tiddler.title, true);
	}
};

abego.parseNamedParameter = function(name, parameter, i) {
	var beginExpression = null;
	if ((i < parameter.length) && parameter[i] == name) {
		i++;
		if (i >= parameter.length) {
			throw "Missing text behind '%0'".format([name]);
		}

		return config.macros.forEachTiddler.paramEncode(parameter[i]);
	}
	return null;
}

// Internal.
//
// --- The write Action ---------------------------------------------------
//
config.macros.forEachTiddler.actions.write.handler = function(place, tiddlers, parameter, context) {
	// Parse the parameter
	var p = 0;
	if (p >= parameter.length) {
		this.handleError(place, "Missing expression behind 'write'.");
		return;
	}

	var textExpression = config.macros.forEachTiddler.paramEncode(parameter[p]);
	p++;

	// Parse the "begin" option
	var beginExpression = abego.parseNamedParameter("begin", parameter, p);
	if (beginExpression !== null)
		p += 2;
	var endExpression = abego.parseNamedParameter("end", parameter, p);
	if (endExpression !== null)
		p += 2;
	var noneExpression = abego.parseNamedParameter("none", parameter, p);
	if (noneExpression !== null)
		p += 2;

	// Parse the "toFile" option
	var filename = null;
	var lineSeparator = undefined;
	if ((p < parameter.length) && parameter[p] == "toFile") {
		p++;
		if (p >= parameter.length) {
			this.handleError(place, "Filename expected behind 'toFile' of 'write' action.");
			return;
		}

		filename = config.macros.forEachTiddler.getLocalPath(config.macros.forEachTiddler.paramEncode(parameter[p]));
		p++;
		if ((p < parameter.length) && parameter[p] == "withLineSeparator") {
			p++;
			if (p >= parameter.length) {
				this.handleError(place, "Line separator text expected behind 'withLineSeparator' of 'write' action.");
				return;
			}
			lineSeparator = config.macros.forEachTiddler.paramEncode(parameter[p]);
			p++;
		}
	}

	// Check for extra parameters
	if (parameter.length > p) {
		config.macros.forEachTiddler.createExtraParameterErrorElement(place, "write", parameter, p);
		return;
	}

	// Perform the action.
	var func = config.macros.forEachTiddler.getEvalTiddlerFunction(textExpression, context);
	var count = tiddlers.length;
	var text = "";
	if (count > 0 && beginExpression)
		text += config.macros.forEachTiddler.getEvalTiddlerFunction(beginExpression, context)(undefined, context, count, undefined);

	for (var i = 0; i < count; i++) {
		var tiddler = tiddlers[i];
		text += func(tiddler, context, count, i);
	}

	if (count > 0 && endExpression)
		text += config.macros.forEachTiddler.getEvalTiddlerFunction(endExpression, context)(undefined, context, count, undefined);

	if (count == 0 && noneExpression)
		text += config.macros.forEachTiddler.getEvalTiddlerFunction(noneExpression, context)(undefined, context, count, undefined);


	if (filename) {
		if (lineSeparator !== undefined) {
			lineSeparator = lineSeparator.replace(/\\n/mg, "\n").replace(/\\r/mg, "\r");
			text = text.replace(/\n/mg,lineSeparator);
		}
		saveFile(filename, convertUnicodeToUTF8(text));
	} else {
		var wrapper = createTiddlyElement(place, "span");
		wikify(text, wrapper, null/* highlightRegExp */, context.inTiddler);
	}
};


// ---------------------------------------------------------------------------
//  Helpers
// ---------------------------------------------------------------------------

// Internal.
//
config.macros.forEachTiddler.createContext = function(placeParam, whereClauseParam, sortClauseParam, sortAscendingParam, actionNameParam, actionParameterParam, scriptText, tiddlyWikiPathParam, inTiddlerParam) {
	return {
		place : placeParam,
		whereClause : whereClauseParam,
		sortClause : sortClauseParam,
		sortAscending : sortAscendingParam,
		script : scriptText,
		actionName : actionNameParam,
		actionParameter : actionParameterParam,
		tiddlyWikiPath : tiddlyWikiPathParam,
		inTiddler : inTiddlerParam, // the tiddler containing the <<forEachTiddler ...>> macro call.
		viewerTiddler : config.macros.forEachTiddler.getContainingTiddler(placeParam) // the tiddler showing the forEachTiddler result
	};
};

// Internal.
//
// Returns a TiddlyWiki with the tiddlers loaded from the TiddlyWiki of
// the given path.
//
config.macros.forEachTiddler.loadTiddlyWiki = function(path, idPrefix) {
	if (!idPrefix) {
		idPrefix = "store";
	}
	var lenPrefix = idPrefix.length;

	// Read the content of the given file
	var content = loadFile(this.getLocalPath(path));
	if(content === null) {
		throw "TiddlyWiki '"+path+"' not found.";
	}

	var tiddlyWiki = new TiddlyWiki();

	// Starting with TW 2.2 there is a helper function to import the tiddlers
	if (tiddlyWiki.importTiddlyWiki) {
		if (!tiddlyWiki.importTiddlyWiki(content))
			throw "File '"+path+"' is not a TiddlyWiki.";
		tiddlyWiki.dirty = false;
		return tiddlyWiki;
	}

	// The legacy code, for TW < 2.2

	// Locate the storeArea div's
	var posOpeningDiv = content.indexOf(startSaveArea);
	var posClosingDiv = content.lastIndexOf(endSaveArea);
	if((posOpeningDiv == -1) || (posClosingDiv == -1)) {
		throw "File '"+path+"' is not a TiddlyWiki.";
	}
	var storageText = content.substr(posOpeningDiv + startSaveArea.length, posClosingDiv);

	// Create a "div" element that contains the storage text
	var myStorageDiv = document.createElement("div");
	myStorageDiv.innerHTML = storageText;
	myStorageDiv.normalize();

	// Create all tiddlers in a new TiddlyWiki
	// (following code is modified copy of TiddlyWiki.prototype.loadFromDiv)
	var store = myStorageDiv.childNodes;
	for(var t = 0; t < store.length; t++) {
		var e = store[t];
		var title = null;
		if(e.getAttribute)
			title = e.getAttribute("tiddler");
		if(!title && e.id && e.id.substr(0,lenPrefix) == idPrefix)
			title = e.id.substr(lenPrefix);
		if(title && title !== "") {
			var tiddler = tiddlyWiki.createTiddler(title);
			tiddler.loadFromDiv(e,title);
		}
	}
	tiddlyWiki.dirty = false;

	return tiddlyWiki;
};



// Internal.
//
// Returns a function that has a function body returning the given javaScriptExpression.
// The function has the parameters:
//
//	 (tiddler, context, count, index)
//
config.macros.forEachTiddler.getEvalTiddlerFunction = function (javaScriptExpression, context) {
	var script = context["script"];
	var functionText = "var theFunction = function(tiddler, context, count, index) { return "+javaScriptExpression+"}";
	var fullText = (script ? script+";" : "")+functionText+";theFunction;";
	return eval(fullText);
};

// Internal.
//
config.macros.forEachTiddler.findTiddlers = function(whereClause, context, tiddlyWiki) {
	var result = [];
	var func = config.macros.forEachTiddler.getEvalTiddlerFunction(whereClause, context);
	tiddlyWiki.forEachTiddler(function(title,tiddler) {
		if (func(tiddler, context, undefined, undefined)) {
			result.push(tiddler);
		}
	});
	return result;
};

// Internal.
//
config.macros.forEachTiddler.createExtraParameterErrorElement = function(place, actionName, parameter, firstUnusedIndex) {
	var message = "Extra parameter behind '"+actionName+"':";
	for (var i = firstUnusedIndex; i < parameter.length; i++) {
		message += " "+parameter[i];
	}
	this.handleError(place, message);
};

// Internal.
//
config.macros.forEachTiddler.sortAscending = function(tiddlerA, tiddlerB) {
	var result =
		(tiddlerA.forEachTiddlerSortValue == tiddlerB.forEachTiddlerSortValue)
			? 0
			: (tiddlerA.forEachTiddlerSortValue < tiddlerB.forEachTiddlerSortValue)
			   ? -1
			   : +1;
	return result;
};

// Internal.
//
config.macros.forEachTiddler.sortDescending = function(tiddlerA, tiddlerB) {
	var result =
		(tiddlerA.forEachTiddlerSortValue == tiddlerB.forEachTiddlerSortValue)
			? 0
			: (tiddlerA.forEachTiddlerSortValue < tiddlerB.forEachTiddlerSortValue)
			   ? +1
			   : -1;
	return result;
};

// Internal.
//
config.macros.forEachTiddler.sortTiddlers = function(tiddlers, sortClause, ascending, context) {
	// To avoid evaluating the sortClause whenever two items are compared
	// we pre-calculate the sortValue for every item in the array and store it in a
	// temporary property ("forEachTiddlerSortValue") of the tiddlers.
	var func = config.macros.forEachTiddler.getEvalTiddlerFunction(sortClause, context);
	var count = tiddlers.length;
	var i;
	for (i = 0; i < count; i++) {
		var tiddler = tiddlers[i];
		tiddler.forEachTiddlerSortValue = func(tiddler,context, undefined, undefined);
	}

	// Do the sorting
	tiddlers.sort(ascending ? this.sortAscending : this.sortDescending);

	// Delete the temporary property that holds the sortValue.
	for (i = 0; i < tiddlers.length; i++) {
		delete tiddlers[i].forEachTiddlerSortValue;
	}
};


// Internal.
//
config.macros.forEachTiddler.trace = function(message) {
	displayMessage(message);
};

// Internal.
//
config.macros.forEachTiddler.traceMacroCall = function(place,macroName,params) {
	var message ="<<"+macroName;
	for (var i = 0; i < params.length; i++) {
		message += " "+params[i];
	}
	message += ">>";
	displayMessage(message);
};


// Internal.
//
// Creates an element that holds an error message
//
config.macros.forEachTiddler.createErrorElement = function(place, exception) {
	var message = (exception.description) ? exception.description : exception.toString();
	return createTiddlyElement(place,"span",null,"forEachTiddlerError","<<forEachTiddler ...>>: "+message);
};

// Internal.
//
// @param place [may be null]
//
config.macros.forEachTiddler.handleError = function(place, exception) {
	if (place) {
		this.createErrorElement(place, exception);
	} else {
		throw exception;
	}
};

// Internal.
//
// Encodes the given string.
//
// Replaces
//	 "$))" to ">>"
//	 "$)" to ">"
//
config.macros.forEachTiddler.paramEncode = function(s) {
	var reGTGT = new RegExp("\\$\\)\\)","mg");
	var reGT = new RegExp("\\$\\)","mg");
	return s.replace(reGTGT, ">>").replace(reGT, ">");
};

// Internal.
//
// Returns the given original path (that is a file path, starting with "file:")
// as a path to a local file, in the systems native file format.
//
// Location information in the originalPath (i.e. the "#" and stuff following)
// is stripped.
//
config.macros.forEachTiddler.getLocalPath = function(originalPath) {
	// Remove any location part of the URL
	var hashPos = originalPath.indexOf("#");
	if(hashPos != -1)
		originalPath = originalPath.substr(0,hashPos);
	// Convert to a native file format assuming
	// "file:///x:/path/path/path..." - pc local file --> "x:\path\path\path..."
	// "file://///server/share/path/path/path..." - FireFox pc network file --> "\\server\share\path\path\path..."
	// "file:///path/path/path..." - mac/unix local file --> "/path/path/path..."
	// "file://server/share/path/path/path..." - pc network file --> "\\server\share\path\path\path..."
	var localPath;
	if(originalPath.charAt(9) == ":") // pc local file
		localPath = unescape(originalPath.substr(8)).replace(new RegExp("/","g"),"\\");
	else if(originalPath.indexOf("file://///") === 0) // FireFox pc network file
		localPath = "\\\\" + unescape(originalPath.substr(10)).replace(new RegExp("/","g"),"\\");
	else if(originalPath.indexOf("file:///") === 0) // mac/unix local file
		localPath = unescape(originalPath.substr(7));
	else if(originalPath.indexOf("file:/") === 0) // mac/unix local file
		localPath = unescape(originalPath.substr(5));
	else // pc network file
		localPath = "\\\\" + unescape(originalPath.substr(7)).replace(new RegExp("/","g"),"\\");
	return localPath;
};

// ---------------------------------------------------------------------------
// Stylesheet Extensions (may be overridden by local StyleSheet)
// ---------------------------------------------------------------------------
//
setStylesheet(
	".forEachTiddlerError{color: #ffffff;background-color: #880000;}",
	"forEachTiddler");

//============================================================================
// End of forEachTiddler Macro
//============================================================================


//============================================================================
// String.startsWith Function
//============================================================================
//
// Returns true if the string starts with the given prefix, false otherwise.
//
version.extensions["String.startsWith"] = {major: 1, minor: 0, revision: 0, date: new Date(2005,11,20), provider: "http://tiddlywiki.abego-software.de"};
//
String.prototype.startsWith = function(prefix) {
	var n =  prefix.length;
	return (this.length >= n) && (this.slice(0, n) == prefix);
};



//============================================================================
// String.endsWith Function
//============================================================================
//
// Returns true if the string ends with the given suffix, false otherwise.
//
version.extensions["String.endsWith"] = {major: 1, minor: 0, revision: 0, date: new Date(2005,11,20), provider: "http://tiddlywiki.abego-software.de"};
//
String.prototype.endsWith = function(suffix) {
	var n = suffix.length;
	return (this.length >= n) && (this.right(n) == suffix);
};


//============================================================================
// String.contains Function
//============================================================================
//
// Returns true when the string contains the given substring, false otherwise.
//
version.extensions["String.contains"] = {major: 1, minor: 0, revision: 0, date: new Date(2005,11,20), provider: "http://tiddlywiki.abego-software.de"};
//
String.prototype.contains = function(substring) {
	return this.indexOf(substring) >= 0;
};

//============================================================================
// Array.indexOf Function
//============================================================================
//
// Returns the index of the first occurance of the given item in the array or
// -1 when no such item exists.
//
// @param item [may be null]
//
version.extensions["Array.indexOf"] = {major: 1, minor: 0, revision: 0, date: new Date(2005,11,20), provider: "http://tiddlywiki.abego-software.de"};
//
Array.prototype.indexOf = function(item) {
	for (var i = 0; i < this.length; i++) {
		if (this[i] == item) {
			return i;
		}
	}
	return -1;
};

//============================================================================
// Array.contains Function
//============================================================================
//
// Returns true when the array contains the given item, otherwise false.
//
// @param item [may be null]
//
version.extensions["Array.contains"] = {major: 1, minor: 0, revision: 0, date: new Date(2005,11,20), provider: "http://tiddlywiki.abego-software.de"};
//
Array.prototype.contains = function(item) {
	return (this.indexOf(item) >= 0);
};

//============================================================================
// Array.containsAny Function
//============================================================================
//
// Returns true when the array contains at least one of the elements
// of the item. Otherwise (or when items contains no elements) false is returned.
//
version.extensions["Array.containsAny"] = {major: 1, minor: 0, revision: 0, date: new Date(2005,11,20), provider: "http://tiddlywiki.abego-software.de"};
//
Array.prototype.containsAny = function(items) {
	for(var i = 0; i < items.length; i++) {
		if (this.contains(items[i])) {
			return true;
		}
	}
	return false;
};


//============================================================================
// Array.containsAll Function
//============================================================================
//
// Returns true when the array contains all the items, otherwise false.
//
// When items is null false is returned (even if the array contains a null).
//
// @param items [may be null]
//
version.extensions["Array.containsAll"] = {major: 1, minor: 0, revision: 0, date: new Date(2005,11,20), provider: "http://tiddlywiki.abego-software.de"};
//
Array.prototype.containsAll = function(items) {
	for(var i = 0; i < items.length; i++) {
		if (!this.contains(items[i])) {
			return false;
		}
	}
	return true;
};


} // of "install only once"

// Used Globals (for JSLint) ==============
// ... DOM
/*global 	document */
// ... TiddlyWiki Core
/*global 	convertUnicodeToUTF8, createTiddlyElement, createTiddlyLink,
			displayMessage, endSaveArea, hasClass, loadFile, saveFile,
			startSaveArea, store, wikify */
//}}}


/***
!Licence and Copyright
Copyright (c) abego Software ~GmbH, 2005 ([[www.abego-software.de|http://www.abego-software.de]])

Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:

Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.

Redistributions in binary form must reproduce the above copyright notice, this
list of conditions and the following disclaimer in the documentation and/or other
materials provided with the distribution.

Neither the name of abego Software nor the names of its contributors may be
used to endorse or promote products derived from this software without specific
prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT
SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
DAMAGE.
***/
/***
|Name|InlineJavascriptPlugin|
|Source|http://www.TiddlyTools.com/#InlineJavascriptPlugin|
|Documentation|http://www.TiddlyTools.com/#InlineJavascriptPluginInfo|
|Version|1.9.6|
|Author|Eric Shulman|
|License|http://www.TiddlyTools.com/#LegalStatements|
|~CoreVersion|2.1|
|Type|plugin|
|Description|Insert Javascript executable code directly into your tiddler content.|
''Call directly into TW core utility routines, define new functions, calculate values, add dynamically-generated TiddlyWiki-formatted output'' into tiddler content, or perform any other programmatic actions each time the tiddler is rendered.
!!!!!Documentation
>see [[InlineJavascriptPluginInfo]]
!!!!!Revisions
<<<
2010.12.15 1.9.6 allow (but ignore) type="..." syntax
|please see [[InlineJavascriptPluginInfo]] for additional revision details|
2005.11.08 1.0.0 initial release
<<<
!!!!!Code
***/
//{{{
version.extensions.InlineJavascriptPlugin= {major: 1, minor: 9, revision: 6, date: new Date(2010,12,15)};

config.formatters.push( {
	name: "inlineJavascript",
	match: "\\<script",
	lookahead: "\\<script(?: type=\\\"[^\\\"]*\\\")?(?: src=\\\"([^\\\"]*)\\\")?(?: label=\\\"([^\\\"]*)\\\")?(?: title=\\\"([^\\\"]*)\\\")?(?: key=\\\"([^\\\"]*)\\\")?( show)?\\>((?:.|\\n)*?)\\</script\\>",
	handler: function(w) {
		var lookaheadRegExp = new RegExp(this.lookahead,"mg");
		lookaheadRegExp.lastIndex = w.matchStart;
		var lookaheadMatch = lookaheadRegExp.exec(w.source)
		if(lookaheadMatch && lookaheadMatch.index == w.matchStart) {
			var src=lookaheadMatch[1];
			var label=lookaheadMatch[2];
			var tip=lookaheadMatch[3];
			var key=lookaheadMatch[4];
			var show=lookaheadMatch[5];
			var code=lookaheadMatch[6];
			if (src) { // external script library
				var script = document.createElement("script"); script.src = src;
				document.body.appendChild(script); document.body.removeChild(script);
			}
			if (code) { // inline code
				if (show) // display source in tiddler
					wikify("{{{\n"+lookaheadMatch[0]+"\n}}}\n",w.output);
				if (label) { // create 'onclick' command link
					var link=createTiddlyElement(w.output,"a",null,"tiddlyLinkExisting",wikifyPlainText(label));
					var fixup=code.replace(/document.write\s*\(/gi,'place.bufferedHTML+=(');
					link.code="function _out(place,tiddler){"+fixup+"\n};_out(this,this.tiddler);"
					link.tiddler=w.tiddler;
					link.onclick=function(){
						this.bufferedHTML="";
						try{ var r=eval(this.code);
							if(this.bufferedHTML.length || (typeof(r)==="string")&&r.length)
								var s=this.parentNode.insertBefore(document.createElement("span"),this.nextSibling);
							if(this.bufferedHTML.length)
								s.innerHTML=this.bufferedHTML;
							if((typeof(r)==="string")&&r.length) {
								wikify(r,s,null,this.tiddler);
								return false;
							} else return r!==undefined?r:false;
						} catch(e){alert(e.description||e.toString());return false;}
					};
					link.setAttribute("title",tip||"");
					var URIcode='javascript:void(eval(decodeURIComponent(%22(function(){try{';
					URIcode+=encodeURIComponent(encodeURIComponent(code.replace(/\n/g,' ')));
					URIcode+='}catch(e){alert(e.description||e.toString())}})()%22)))';
					link.setAttribute("href",URIcode);
					link.style.cursor="pointer";
					if (key) link.accessKey=key.substr(0,1); // single character only
				}
				else { // run script immediately
					var fixup=code.replace(/document.write\s*\(/gi,'place.innerHTML+=(');
					var c="function _out(place,tiddler){"+fixup+"\n};_out(w.output,w.tiddler);";
					try	 { var out=eval(c); }
					catch(e) { out=e.description?e.description:e.toString(); }
					if (out && out.length) wikify(out,w.output,w.highlightRegExp,w.tiddler);
				}
			}
			w.nextMatch = lookaheadMatch.index + lookaheadMatch[0].length;
		}
	}
} )
//}}}

// // Backward-compatibility for TW2.1.x and earlier
//{{{
if (typeof(wikifyPlainText)=="undefined") window.wikifyPlainText=function(text,limit,tiddler) {
	if(limit > 0) text = text.substr(0,limit);
	var wikifier = new Wikifier(text,formatter,null,tiddler);
	return wikifier.wikifyPlain();
}
//}}}

// // GLOBAL FUNCTION: $(...) -- 'shorthand' convenience syntax for document.getElementById()
//{{{
if (typeof($)=='undefined') { function $(id) { return document.getElementById(id.replace(/^#/,'')); } }
//}}}
/***
|''Name:''|LoadRemoteFileThroughProxy (previous LoadRemoteFileHijack)|
|''Description:''|When the TiddlyWiki file is located on the web (view over http) the content of [[SiteProxy]] tiddler is added in front of the file url. If [[SiteProxy]] does not exist "/proxy/" is added. |
|''Version:''|1.1.0|
|''Date:''|mar 17, 2007|
|''Source:''|http://tiddlywiki.bidix.info/#LoadRemoteFileHijack|
|''Author:''|BidiX (BidiX (at) bidix (dot) info)|
|''License:''|[[BSD open source license|http://tiddlywiki.bidix.info/#%5B%5BBSD%20open%20source%20license%5D%5D ]]|
|''~CoreVersion:''|2.2.0|
***/
//{{{
version.extensions.LoadRemoteFileThroughProxy = {
 major: 1, minor: 1, revision: 0, 
 date: new Date("mar 17, 2007"), 
 source: "http://tiddlywiki.bidix.info/#LoadRemoteFileThroughProxy"};

if (!window.bidix) window.bidix = {}; // bidix namespace
if (!bidix.core) bidix.core = {};

bidix.core.loadRemoteFile = loadRemoteFile;
loadRemoteFile = function(url,callback,params)
{
 if ((document.location.toString().substr(0,4) == "http") && (url.substr(0,4) == "http")){ 
 url = store.getTiddlerText("SiteProxy", "/proxy/") + url;
 }
 return bidix.core.loadRemoteFile(url,callback,params);
}
//}}}
<script>
var count = "ct_counter";          // Change Your Account?
var type = "7segamberled";       // Change Your Counter Image?
var digits = "6";          // Change The Amount of Digits on Your Counter?
var prog = "hit";          // Change to Either hit/unique?
var statslink = "no";    // provide statistical link in counter yes/no?
var sitelink = "yes";     // provide link back to our site;~) yes/no?
var cntvisible = "yes"; // do you want counter visible yes/no?
</script>
<script src="http://005.free-counters.co.uk/count-089.js">
</script>
<noscript>
<a href="http://www.free-counters.co.uk" target="_blank">
<img  src="http://005.free-counters.co.uk/count-089.pl?count=ct_counter&cntvisible=no&mode=noscript" alt="Free Counters" title="Free Counters" border="0">
</a>The following text will not be seen after you upload your website,
please keep it in order to retain your counter functionality 
<br><a href="http://www.free-counters.co.uk/trackers/" target="_blank">Trackers</a><br> <a href="http://www.free-counters.co.uk/help/counter/" target="_blank">Counter Help</a><br>

</noscript>

/***
|Name|NestedSlidersPlugin|
|Source|http://www.TiddlyTools.com/#NestedSlidersPlugin|
|Documentation|http://www.TiddlyTools.com/#NestedSlidersPluginInfo|
|Version|2.4.9|
|Author|Eric Shulman|
|License|http://www.TiddlyTools.com/#LegalStatements|
|~CoreVersion|2.1|
|Type|plugin|
|Description|show content in nest-able sliding/floating panels, without creating separate tiddlers for each panel's content|
!!!!!Documentation
>see [[NestedSlidersPluginInfo]]
!!!!!Configuration
<<<
<<option chkFloatingSlidersAnimate>> allow floating sliders to animate when opening/closing
>Note: This setting can cause 'clipping' problems in some versions of InternetExplorer.
>In addition, for floating slider animation to occur you must also allow animation in general (see [[AdvancedOptions]]).
<<<
!!!!!Revisions
<<<
2008.11.15 - 2.4.9 in adjustNestedSlider(), don't make adjustments if panel is marked as 'undocked' (CSS class).  In onClickNestedSlider(), SHIFT-CLICK docks panel (see [[MoveablePanelPlugin]])
|please see [[NestedSlidersPluginInfo]] for additional revision details|
2005.11.03 - 1.0.0 initial public release.  Thanks to RodneyGomes, GeoffSlocock, and PaulPetterson for suggestions and experiments.
<<<
!!!!!Code
***/
//{{{
version.extensions.NestedSlidersPlugin= {major: 2, minor: 4, revision: 9, date: new Date(2008,11,15)};

// options for deferred rendering of sliders that are not initially displayed
if (config.options.chkFloatingSlidersAnimate===undefined)
	config.options.chkFloatingSlidersAnimate=false; // avoid clipping problems in IE

// default styles for 'floating' class
setStylesheet(".floatingPanel { position:absolute; z-index:10; padding:0.5em; margin:0em; \
	background-color:#eee; color:#000; border:1px solid #000; text-align:left; }","floatingPanelStylesheet");

// if removeCookie() function is not defined by TW core, define it here.
if (window.removeCookie===undefined) {
	window.removeCookie=function(name) {
		document.cookie = name+'=; expires=Thu, 01-Jan-1970 00:00:01 UTC; path=/;'; 
	}
}

config.formatters.push( {
	name: "nestedSliders",
	match: "\\n?\\+{3}",
	terminator: "\\s*\\={3}\\n?",
	lookahead: "\\n?\\+{3}(\\+)?(\\([^\\)]*\\))?(\\!*)?(\\^(?:[^\\^\\*\\@\\[\\>]*\\^)?)?(\\*)?(\\@)?(?:\\{\\{([\\w]+[\\s\\w]*)\\{)?(\\[[^\\]]*\\])?(\\[[^\\]]*\\])?(?:\\}{3})?(\\#[^:]*\\:)?(\\>)?(\\.\\.\\.)?\\s*",
	handler: function(w)
		{
			lookaheadRegExp = new RegExp(this.lookahead,"mg");
			lookaheadRegExp.lastIndex = w.matchStart;
			var lookaheadMatch = lookaheadRegExp.exec(w.source)
			if(lookaheadMatch && lookaheadMatch.index == w.matchStart)
			{
				var defopen=lookaheadMatch[1];
				var cookiename=lookaheadMatch[2];
				var header=lookaheadMatch[3];
				var panelwidth=lookaheadMatch[4];
				var transient=lookaheadMatch[5];
				var hover=lookaheadMatch[6];
				var buttonClass=lookaheadMatch[7];
				var label=lookaheadMatch[8];
				var openlabel=lookaheadMatch[9];
				var panelID=lookaheadMatch[10];
				var blockquote=lookaheadMatch[11];
				var deferred=lookaheadMatch[12];

				// location for rendering button and panel
				var place=w.output;

				// default to closed, no cookie, no accesskey, no alternate text/tip
				var show="none"; var cookie=""; var key="";
				var closedtext=">"; var closedtip="";
				var openedtext="<"; var openedtip="";

				// extra "+", default to open
				if (defopen) show="block";

				// cookie, use saved open/closed state
				if (cookiename) {
					cookie=cookiename.trim().slice(1,-1);
					cookie="chkSlider"+cookie;
					if (config.options[cookie]==undefined)
						{ config.options[cookie] = (show=="block") }
					show=config.options[cookie]?"block":"none";
				}

				// parse label/tooltip/accesskey: [label=X|tooltip]
				if (label) {
					var parts=label.trim().slice(1,-1).split("|");
					closedtext=parts.shift();
					if (closedtext.substr(closedtext.length-2,1)=="=")	
						{ key=closedtext.substr(closedtext.length-1,1); closedtext=closedtext.slice(0,-2); }
					openedtext=closedtext;
					if (parts.length) closedtip=openedtip=parts.join("|");
					else { closedtip="show "+closedtext; openedtip="hide "+closedtext; }
				}

				// parse alternate label/tooltip: [label|tooltip]
				if (openlabel) {
					var parts=openlabel.trim().slice(1,-1).split("|");
					openedtext=parts.shift();
					if (parts.length) openedtip=parts.join("|");
					else openedtip="hide "+openedtext;
				}

				var title=show=='block'?openedtext:closedtext;
				var tooltip=show=='block'?openedtip:closedtip;

				// create the button
				if (header) { // use "Hn" header format instead of button/link
					var lvl=(header.length>5)?5:header.length;
					var btn = createTiddlyElement(createTiddlyElement(place,"h"+lvl,null,null,null),"a",null,buttonClass,title);
					btn.onclick=onClickNestedSlider;
					btn.setAttribute("href","javascript:;");
					btn.setAttribute("title",tooltip);
				}
				else
					var btn = createTiddlyButton(place,title,tooltip,onClickNestedSlider,buttonClass);
				btn.innerHTML=title; // enables use of HTML entities in label

				// set extra button attributes
				btn.setAttribute("closedtext",closedtext);
				btn.setAttribute("closedtip",closedtip);
				btn.setAttribute("openedtext",openedtext);
				btn.setAttribute("openedtip",openedtip);
				btn.sliderCookie = cookie; // save the cookiename (if any) in the button object
				btn.defOpen=defopen!=null; // save default open/closed state (boolean)
				btn.keyparam=key; // save the access key letter ("" if none)
				if (key.length) {
					btn.setAttribute("accessKey",key); // init access key
					btn.onfocus=function(){this.setAttribute("accessKey",this.keyparam);}; // **reclaim** access key on focus
				}
				btn.setAttribute("hover",hover?"true":"false");
				btn.onmouseover=function(ev) {
					// optional 'open on hover' handling
					if (this.getAttribute("hover")=="true" && this.sliderPanel.style.display=='none') {
						document.onclick.call(document,ev); // close transients
						onClickNestedSlider(ev); // open this slider
					}
					// mouseover on button aligns floater position with button
					if (window.adjustSliderPos) window.adjustSliderPos(this.parentNode,this,this.sliderPanel);
				}

				// create slider panel
				var panelClass=panelwidth?"floatingPanel":"sliderPanel";
				if (panelID) panelID=panelID.slice(1,-1); // trim off delimiters
				var panel=createTiddlyElement(place,"div",panelID,panelClass,null);
				panel.button = btn; // so the slider panel know which button it belongs to
				btn.sliderPanel=panel; // so the button knows which slider panel it belongs to
				panel.defaultPanelWidth=(panelwidth && panelwidth.length>2)?panelwidth.slice(1,-1):"";
				panel.setAttribute("transient",transient=="*"?"true":"false");
				panel.style.display = show;
				panel.style.width=panel.defaultPanelWidth;
				panel.onmouseover=function(event) // mouseover on panel aligns floater position with button
					{ if (window.adjustSliderPos) window.adjustSliderPos(this.parentNode,this.button,this); }

				// render slider (or defer until shown) 
				w.nextMatch = lookaheadMatch.index + lookaheadMatch[0].length;
				if ((show=="block")||!deferred) {
					// render now if panel is supposed to be shown or NOT deferred rendering
					w.subWikify(blockquote?createTiddlyElement(panel,"blockquote"):panel,this.terminator);
					// align floater position with button
					if (window.adjustSliderPos) window.adjustSliderPos(place,btn,panel);
				}
				else {
					var src = w.source.substr(w.nextMatch);
					var endpos=findMatchingDelimiter(src,"+++","===");
					panel.setAttribute("raw",src.substr(0,endpos));
					panel.setAttribute("blockquote",blockquote?"true":"false");
					panel.setAttribute("rendered","false");
					w.nextMatch += endpos+3;
					if (w.source.substr(w.nextMatch,1)=="\n") w.nextMatch++;
				}
			}
		}
	}
)

function findMatchingDelimiter(src,starttext,endtext) {
	var startpos = 0;
	var endpos = src.indexOf(endtext);
	// check for nested delimiters
	while (src.substring(startpos,endpos-1).indexOf(starttext)!=-1) {
		// count number of nested 'starts'
		var startcount=0;
		var temp = src.substring(startpos,endpos-1);
		var pos=temp.indexOf(starttext);
		while (pos!=-1)  { startcount++; pos=temp.indexOf(starttext,pos+starttext.length); }
		// set up to check for additional 'starts' after adjusting endpos
		startpos=endpos+endtext.length;
		// find endpos for corresponding number of matching 'ends'
		while (startcount && endpos!=-1) {
			endpos = src.indexOf(endtext,endpos+endtext.length);
			startcount--;
		}
	}
	return (endpos==-1)?src.length:endpos;
}
//}}}
//{{{
window.onClickNestedSlider=function(e)
{
	if (!e) var e = window.event;
	var theTarget = resolveTarget(e);
	while (theTarget && theTarget.sliderPanel==undefined) theTarget=theTarget.parentNode;
	if (!theTarget) return false;
	var theSlider = theTarget.sliderPanel;
	var isOpen = theSlider.style.display!="none";

	// if SHIFT-CLICK, dock panel first (see [[MoveablePanelPlugin]])
	if (e.shiftKey && config.macros.moveablePanel) config.macros.moveablePanel.dock(theSlider,e);

	// toggle label
	theTarget.innerHTML=isOpen?theTarget.getAttribute("closedText"):theTarget.getAttribute("openedText");
	// toggle tooltip
	theTarget.setAttribute("title",isOpen?theTarget.getAttribute("closedTip"):theTarget.getAttribute("openedTip"));

	// deferred rendering (if needed)
	if (theSlider.getAttribute("rendered")=="false") {
		var place=theSlider;
		if (theSlider.getAttribute("blockquote")=="true")
			place=createTiddlyElement(place,"blockquote");
		wikify(theSlider.getAttribute("raw"),place);
		theSlider.setAttribute("rendered","true");
	}

	// show/hide the slider
	if(config.options.chkAnimate && (!hasClass(theSlider,'floatingPanel') || config.options.chkFloatingSlidersAnimate))
		anim.startAnimating(new Slider(theSlider,!isOpen,e.shiftKey || e.altKey,"none"));
	else
		theSlider.style.display = isOpen ? "none" : "block";

	// reset to default width (might have been changed via plugin code)
	theSlider.style.width=theSlider.defaultPanelWidth;

	// align floater panel position with target button
	if (!isOpen && window.adjustSliderPos) window.adjustSliderPos(theSlider.parentNode,theTarget,theSlider);

	// if showing panel, set focus to first 'focus-able' element in panel
	if (theSlider.style.display!="none") {
		var ctrls=theSlider.getElementsByTagName("*");
		for (var c=0; c<ctrls.length; c++) {
			var t=ctrls[c].tagName.toLowerCase();
			if ((t=="input" && ctrls[c].type!="hidden") || t=="textarea" || t=="select")
				{ try{ ctrls[c].focus(); } catch(err){;} break; }
		}
	}
	var cookie=theTarget.sliderCookie;
	if (cookie && cookie.length) {
		config.options[cookie]=!isOpen;
		if (config.options[cookie]!=theTarget.defOpen) window.saveOptionCookie(cookie);
		else window.removeCookie(cookie); // remove cookie if slider is in default display state
	}

	// prevent SHIFT-CLICK from being processed by browser (opens blank window... yuck!)
	// prevent clicks *within* a slider button from being processed by browser
	// but allow plain click to bubble up to page background (to close transients, if any)
	if (e.shiftKey || theTarget!=resolveTarget(e))
		{ e.cancelBubble=true; if (e.stopPropagation) e.stopPropagation(); }
	Popup.remove(); // close open popup (if any)
	return false;
}
//}}}
//{{{
// click in document background closes transient panels 
document.nestedSliders_savedOnClick=document.onclick;
document.onclick=function(ev) { if (!ev) var ev=window.event; var target=resolveTarget(ev);

	if (document.nestedSliders_savedOnClick)
		var retval=document.nestedSliders_savedOnClick.apply(this,arguments);
	// if click was inside a popup... leave transient panels alone
	var p=target; while (p) if (hasClass(p,"popup")) break; else p=p.parentNode;
	if (p) return retval;
	// if click was inside transient panel (or something contained by a transient panel), leave it alone
	var p=target; while (p) {
		if ((hasClass(p,"floatingPanel")||hasClass(p,"sliderPanel"))&&p.getAttribute("transient")=="true") break;
		p=p.parentNode;
	}
	if (p) return retval;
	// otherwise, find and close all transient panels...
	var all=document.all?document.all:document.getElementsByTagName("DIV");
	for (var i=0; i<all.length; i++) {
		 // if it is not a transient panel, or the click was on the button that opened this panel, don't close it.
		if (all[i].getAttribute("transient")!="true" || all[i].button==target) continue;
		// otherwise, if the panel is currently visible, close it by clicking it's button
		if (all[i].style.display!="none") window.onClickNestedSlider({target:all[i].button})
		if (!hasClass(all[i],"floatingPanel")&&!hasClass(all[i],"sliderPanel")) all[i].style.display="none";
	}
	return retval;
};
//}}}
//{{{
// adjust floating panel position based on button position
if (window.adjustSliderPos==undefined) window.adjustSliderPos=function(place,btn,panel) {
	if (hasClass(panel,"floatingPanel") && !hasClass(panel,"undocked")) {
		// see [[MoveablePanelPlugin]] for use of 'undocked'
		var rightEdge=document.body.offsetWidth-1;
		var panelWidth=panel.offsetWidth;
		var left=0;
		var top=btn.offsetHeight; 
		if (place.style.position=="relative" && findPosX(btn)+panelWidth>rightEdge) {
			left-=findPosX(btn)+panelWidth-rightEdge; // shift panel relative to button
			if (findPosX(btn)+left<0) left=-findPosX(btn); // stay within left edge
		}
		if (place.style.position!="relative") {
			var left=findPosX(btn);
			var top=findPosY(btn)+btn.offsetHeight;
			var p=place; while (p && !hasClass(p,'floatingPanel')) p=p.parentNode;
			if (p) { left-=findPosX(p); top-=findPosY(p); }
			if (left+panelWidth>rightEdge) left=rightEdge-panelWidth;
			if (left<0) left=0;
		}
		panel.style.left=left+"px"; panel.style.top=top+"px";
	}
}
//}}}
//{{{
// TW2.1 and earlier:
// hijack Slider stop handler so overflow is visible after animation has completed
Slider.prototype.coreStop = Slider.prototype.stop;
Slider.prototype.stop = function()
	{ this.coreStop.apply(this,arguments); this.element.style.overflow = "visible"; }

// TW2.2+
// hijack Morpher stop handler so sliderPanel/floatingPanel overflow is visible after animation has completed
if (version.major+.1*version.minor+.01*version.revision>=2.2) {
	Morpher.prototype.coreStop = Morpher.prototype.stop;
	Morpher.prototype.stop = function() {
		this.coreStop.apply(this,arguments);
		var e=this.element;
		if (hasClass(e,"sliderPanel")||hasClass(e,"floatingPanel")) {
			// adjust panel overflow and position after animation
			e.style.overflow = "visible";
			if (window.adjustSliderPos) window.adjustSliderPos(e.parentNode,e.button,e);
		}
	};
}
//}}}
/***
|''Name:''|PasswordOptionPlugin|
|''Description:''|Extends TiddlyWiki options with non encrypted password option.|
|''Version:''|1.0.2|
|''Date:''|Apr 19, 2007|
|''Source:''|http://tiddlywiki.bidix.info/#PasswordOptionPlugin|
|''Author:''|BidiX (BidiX (at) bidix (dot) info)|
|''License:''|[[BSD open source license|http://tiddlywiki.bidix.info/#%5B%5BBSD%20open%20source%20license%5D%5D ]]|
|''~CoreVersion:''|2.2.0 (Beta 5)|
***/
//{{{
version.extensions.PasswordOptionPlugin = {
	major: 1, minor: 0, revision: 2, 
	date: new Date("Apr 19, 2007"),
	source: 'http://tiddlywiki.bidix.info/#PasswordOptionPlugin',
	author: 'BidiX (BidiX (at) bidix (dot) info',
	license: '[[BSD open source license|http://tiddlywiki.bidix.info/#%5B%5BBSD%20open%20source%20license%5D%5D]]',
	coreVersion: '2.2.0 (Beta 5)'
};

config.macros.option.passwordCheckboxLabel = "Save this password on this computer";
config.macros.option.passwordInputType = "password"; // password | text
setStylesheet(".pasOptionInput {width: 11em;}\n","passwordInputTypeStyle");

merge(config.macros.option.types, {
	'pas': {
		elementType: "input",
		valueField: "value",
		eventName: "onkeyup",
		className: "pasOptionInput",
		typeValue: config.macros.option.passwordInputType,
		create: function(place,type,opt,className,desc) {
			// password field
			config.macros.option.genericCreate(place,'pas',opt,className,desc);
			// checkbox linked with this password "save this password on this computer"
			config.macros.option.genericCreate(place,'chk','chk'+opt,className,desc);			
			// text savePasswordCheckboxLabel
			place.appendChild(document.createTextNode(config.macros.option.passwordCheckboxLabel));
		},
		onChange: config.macros.option.genericOnChange
	}
});

merge(config.optionHandlers['chk'], {
	get: function(name) {
		// is there an option linked with this chk ?
		var opt = name.substr(3);
		if (config.options[opt]) 
			saveOptionCookie(opt);
		return config.options[name] ? "true" : "false";
	}
});

merge(config.optionHandlers, {
	'pas': {
 		get: function(name) {
			if (config.options["chk"+name]) {
				return encodeCookie(config.options[name].toString());
			} else {
				return "";
			}
		},
		set: function(name,value) {config.options[name] = decodeCookie(value);}
	}
});

// need to reload options to load passwordOptions
loadOptionsCookie();

/*
if (!config.options['pasPassword'])
	config.options['pasPassword'] = '';

merge(config.optionsDesc,{
		pasPassword: "Test password"
	});
*/
//}}}
<<drawVisualization>>

<<forEachTiddler 
 where
 '(tiddler.tags.contains("TLData2")||tiddler.tags.contains("TLData3"))'
 script
 '
function addBar(t) {
var ContractNumber = store.getTiddlerSlice(t,"ContractNumber");
if (ContractNumber == "") {
	ContractNumber = store.getTiddlerSlice(t,"Name").slice(0,15);
}

var Name= store.getTiddlerSlice(t,"Name");

var StartDate= store.getTiddlerSlice(t,"StartDate");
var SDate = new Date(StartDate);

var MiddleDate= store.getTiddlerSlice(t,"MiddleDate");
var MDate = new Date(MiddleDate);

var EndDate= store.getTiddlerSlice(t,"EndDate");
var EDate= new Date(EndDate);

if (((Date.parse(StartDate) || 0) > 0) && ((Date.parse(MiddleDate) || 0) > 0)) {
	timeline.addItem({"start": SDate, "end": MDate, "content": "<div style=\"background-color:Orchid; border:0px solid Orchid;padding:0px;\">Start</div>","group": ContractNumber});
	}
if (((Date.parse(MiddleDate) || 0) > 0) && ((Date.parse(EndDate) || 0) > 0)) {
	timeline.addItem({"start": MDate, "end": EDate, "content": "<div style=\"background-color:LawnGreen; border:0px solid Cyan;padding:0px;\">"+Name+"</div>","group": ContractNumber});
	}
timeline.setVisibleChartRangeAuto();
return "";
}
 '
 write
 'addBar(tiddler.title)'
>>

This tiddler demonstrates the use of a script embedded within a ForEachTiddler macro to create the bars on the timeline.  The example provided creates multiple (two) bars per tiddler and looks across two tags.  The slice names are also different to the standard CHAP Timeline slices.  Click edit to view.
<<drawVisualization TLData1>>

This tiddler creates one bar per tiddler.  All tiddlers must have the same tag to be included.  The slices are limited to:
{{{
StartDate:
EndDate:
Color:
Text:
Group:
}}}

~StartDate is mandatory.
~EndDate is optional.
Color is optional.  As color is part of the style of the content, additional style elements may be snuck in here (see Event 5).
Text is optional.  Image tags may be included (see Event 5).
Group is optional.
[[CHAPTimelineCSS]]
/***
Description: Contains the stuff you need to use Tiddlyspot
Note, you also need UploadPlugin, PasswordOptionPlugin and LoadRemoteFileThroughProxy
from http://tiddlywiki.bidix.info for a complete working Tiddlyspot site.
***/
//{{{

// edit this if you are migrating sites or retrofitting an existing TW
config.tiddlyspotSiteId = 'chaptimeline';

// make it so you can by default see edit controls via http
config.options.chkHttpReadOnly = false;
window.readOnly = false; // make sure of it (for tw 2.2)
window.showBackstage = true; // show backstage too

// disable autosave in d3
if (window.location.protocol != "file:")
	config.options.chkGTDLazyAutoSave = false;

// tweak shadow tiddlers to add upload button, password entry box etc
with (config.shadowTiddlers) {
	SiteUrl = 'http://'+config.tiddlyspotSiteId+'.tiddlyspot.com';
	SideBarOptions = SideBarOptions.replace(/(<<saveChanges>>)/,"$1<<tiddler TspotSidebar>>");
	OptionsPanel = OptionsPanel.replace(/^/,"<<tiddler TspotOptions>>");
	DefaultTiddlers = DefaultTiddlers.replace(/^/,"[[WelcomeToTiddlyspot]] ");
	MainMenu = MainMenu.replace(/^/,"[[WelcomeToTiddlyspot]] ");
}

// create some shadow tiddler content
merge(config.shadowTiddlers,{

'TspotOptions':[
 "tiddlyspot password:",
 "<<option pasUploadPassword>>",
 ""
].join("\n"),

'TspotControls':[
 "| tiddlyspot password:|<<option pasUploadPassword>>|",
 "| site management:|<<upload http://" + config.tiddlyspotSiteId + ".tiddlyspot.com/store.cgi index.html . .  " + config.tiddlyspotSiteId + ">>//(requires tiddlyspot password)//<br>[[control panel|http://" + config.tiddlyspotSiteId + ".tiddlyspot.com/controlpanel]], [[download (go offline)|http://" + config.tiddlyspotSiteId + ".tiddlyspot.com/download]]|",
 "| links:|[[tiddlyspot.com|http://tiddlyspot.com/]], [[FAQs|http://faq.tiddlyspot.com/]], [[blog|http://tiddlyspot.blogspot.com/]], email [[support|mailto:support@tiddlyspot.com]] & [[feedback|mailto:feedback@tiddlyspot.com]], [[donate|http://tiddlyspot.com/?page=donate]]|"
].join("\n"),

'WelcomeToTiddlyspot':[
 "This document is a ~TiddlyWiki from tiddlyspot.com.  A ~TiddlyWiki is an electronic notebook that is great for managing todo lists, personal information, and all sorts of things.",
 "",
 "@@font-weight:bold;font-size:1.3em;color:#444; //What now?// &nbsp;&nbsp;@@ Before you can save any changes, you need to enter your password in the form below.  Then configure privacy and other site settings at your [[control panel|http://" + config.tiddlyspotSiteId + ".tiddlyspot.com/controlpanel]] (your control panel username is //" + config.tiddlyspotSiteId + "//).",
 "<<tiddler TspotControls>>",
 "See also GettingStarted.",
 "",
 "@@font-weight:bold;font-size:1.3em;color:#444; //Working online// &nbsp;&nbsp;@@ You can edit this ~TiddlyWiki right now, and save your changes using the \"save to web\" button in the column on the right.",
 "",
 "@@font-weight:bold;font-size:1.3em;color:#444; //Working offline// &nbsp;&nbsp;@@ A fully functioning copy of this ~TiddlyWiki can be saved onto your hard drive or USB stick.  You can make changes and save them locally without being connected to the Internet.  When you're ready to sync up again, just click \"upload\" and your ~TiddlyWiki will be saved back to tiddlyspot.com.",
 "",
 "@@font-weight:bold;font-size:1.3em;color:#444; //Help!// &nbsp;&nbsp;@@ Find out more about ~TiddlyWiki at [[TiddlyWiki.com|http://tiddlywiki.com]].  Also visit [[TiddlyWiki.org|http://tiddlywiki.org]] for documentation on learning and using ~TiddlyWiki. New users are especially welcome on the [[TiddlyWiki mailing list|http://groups.google.com/group/TiddlyWiki]], which is an excellent place to ask questions and get help.  If you have a tiddlyspot related problem email [[tiddlyspot support|mailto:support@tiddlyspot.com]].",
 "",
 "@@font-weight:bold;font-size:1.3em;color:#444; //Enjoy :)// &nbsp;&nbsp;@@ We hope you like using your tiddlyspot.com site.  Please email [[feedback@tiddlyspot.com|mailto:feedback@tiddlyspot.com]] with any comments or suggestions."
].join("\n"),

'TspotSidebar':[
 "<<upload http://" + config.tiddlyspotSiteId + ".tiddlyspot.com/store.cgi index.html . .  " + config.tiddlyspotSiteId + ">><html><a href='http://" + config.tiddlyspotSiteId + ".tiddlyspot.com/download' class='button'>download</a></html>"
].join("\n")

});
//}}}
| !date | !user | !location | !storeUrl | !uploadDir | !toFilename | !backupdir | !origin |
| 04/06/2012 17:43:40 | YourName | [[/|http://chaptimeline.tiddlyspot.com/]] | [[store.cgi|http://chaptimeline.tiddlyspot.com/store.cgi]] | . | [[index.html | http://chaptimeline.tiddlyspot.com/index.html]] | . |
| 04/06/2012 17:45:58 | YourName | [[/|http://chaptimeline.tiddlyspot.com/]] | [[store.cgi|http://chaptimeline.tiddlyspot.com/store.cgi]] | . | [[index.html | http://chaptimeline.tiddlyspot.com/index.html]] | . |
| 04/06/2012 17:47:04 | YourName | [[/|http://chaptimeline.tiddlyspot.com/]] | [[store.cgi|http://chaptimeline.tiddlyspot.com/store.cgi]] | . | [[index.html | http://chaptimeline.tiddlyspot.com/index.html]] | . |
| 04/06/2012 17:48:40 | YourName | [[/|http://chaptimeline.tiddlyspot.com/]] | [[store.cgi|http://chaptimeline.tiddlyspot.com/store.cgi]] | . | [[index.html | http://chaptimeline.tiddlyspot.com/index.html]] | . |
| 04/06/2012 17:49:47 | YourName | [[/|http://chaptimeline.tiddlyspot.com/]] | [[store.cgi|http://chaptimeline.tiddlyspot.com/store.cgi]] | . | [[index.html | http://chaptimeline.tiddlyspot.com/index.html]] | . | ok |
| 04/06/2012 17:50:40 | YourName | [[/|http://chaptimeline.tiddlyspot.com/]] | [[store.cgi|http://chaptimeline.tiddlyspot.com/store.cgi]] | . | [[index.html | http://chaptimeline.tiddlyspot.com/index.html]] | . |
| 04/06/2012 17:52:25 | YourName | [[/|http://chaptimeline.tiddlyspot.com/]] | [[store.cgi|http://chaptimeline.tiddlyspot.com/store.cgi]] | . | [[index.html | http://chaptimeline.tiddlyspot.com/index.html]] | . |
| 04/06/2012 11:33:05 | YourName | [[/|http://chaptimeline.tiddlyspot.com/]] | [[store.cgi|http://chaptimeline.tiddlyspot.com/store.cgi]] | . | [[index.html | http://chaptimeline.tiddlyspot.com/index.html]] | . |
| 08/06/2012 12:49:36 | YourName | [[/|http://chaptimeline.tiddlyspot.com/]] | [[store.cgi|http://chaptimeline.tiddlyspot.com/store.cgi]] | . | [[index.html | http://chaptimeline.tiddlyspot.com/index.html]] | . | ok |
| 08/06/2012 12:54:58 | YourName | [[/|http://chaptimeline.tiddlyspot.com/]] | [[store.cgi|http://chaptimeline.tiddlyspot.com/store.cgi]] | . | [[index.html | http://chaptimeline.tiddlyspot.com/index.html]] | . |
/***
|''Name:''|UploadPlugin|
|''Description:''|Save to web a TiddlyWiki|
|''Version:''|4.1.3|
|''Date:''|Feb 24, 2008|
|''Source:''|http://tiddlywiki.bidix.info/#UploadPlugin|
|''Documentation:''|http://tiddlywiki.bidix.info/#UploadPluginDoc|
|''Author:''|BidiX (BidiX (at) bidix (dot) info)|
|''License:''|[[BSD open source license|http://tiddlywiki.bidix.info/#%5B%5BBSD%20open%20source%20license%5D%5D ]]|
|''~CoreVersion:''|2.2.0|
|''Requires:''|PasswordOptionPlugin|
***/
//{{{
version.extensions.UploadPlugin = {
	major: 4, minor: 1, revision: 3,
	date: new Date("Feb 24, 2008"),
	source: 'http://tiddlywiki.bidix.info/#UploadPlugin',
	author: 'BidiX (BidiX (at) bidix (dot) info',
	coreVersion: '2.2.0'
};

//
// Environment
//

if (!window.bidix) window.bidix = {}; // bidix namespace
bidix.debugMode = false;	// true to activate both in Plugin and UploadService
	
//
// Upload Macro
//

config.macros.upload = {
// default values
	defaultBackupDir: '',	//no backup
	defaultStoreScript: "store.php",
	defaultToFilename: "index.html",
	defaultUploadDir: ".",
	authenticateUser: true	// UploadService Authenticate User
};
	
config.macros.upload.label = {
	promptOption: "Save and Upload this TiddlyWiki with UploadOptions",
	promptParamMacro: "Save and Upload this TiddlyWiki in %0",
	saveLabel: "save to web", 
	saveToDisk: "save to disk",
	uploadLabel: "upload"	
};

config.macros.upload.messages = {
	noStoreUrl: "No store URL in parmeters or options",
	usernameOrPasswordMissing: "Username or password missing"
};

config.macros.upload.handler = function(place,macroName,params) {
	if (readOnly)
		return;
	var label;
	if (document.location.toString().substr(0,4) == "http") 
		label = this.label.saveLabel;
	else
		label = this.label.uploadLabel;
	var prompt;
	if (params[0]) {
		prompt = this.label.promptParamMacro.toString().format([this.destFile(params[0], 
			(params[1] ? params[1]:bidix.basename(window.location.toString())), params[3])]);
	} else {
		prompt = this.label.promptOption;
	}
	createTiddlyButton(place, label, prompt, function() {config.macros.upload.action(params);}, null, null, this.accessKey);
};

config.macros.upload.action = function(params)
{
		// for missing macro parameter set value from options
		if (!params) params = {};
		var storeUrl = params[0] ? params[0] : config.options.txtUploadStoreUrl;
		var toFilename = params[1] ? params[1] : config.options.txtUploadFilename;
		var backupDir = params[2] ? params[2] : config.options.txtUploadBackupDir;
		var uploadDir = params[3] ? params[3] : config.options.txtUploadDir;
		var username = params[4] ? params[4] : config.options.txtUploadUserName;
		var password = config.options.pasUploadPassword; // for security reason no password as macro parameter	
		// for still missing parameter set default value
		if ((!storeUrl) && (document.location.toString().substr(0,4) == "http")) 
			storeUrl = bidix.dirname(document.location.toString())+'/'+config.macros.upload.defaultStoreScript;
		if (storeUrl.substr(0,4) != "http")
			storeUrl = bidix.dirname(document.location.toString()) +'/'+ storeUrl;
		if (!toFilename)
			toFilename = bidix.basename(window.location.toString());
		if (!toFilename)
			toFilename = config.macros.upload.defaultToFilename;
		if (!uploadDir)
			uploadDir = config.macros.upload.defaultUploadDir;
		if (!backupDir)
			backupDir = config.macros.upload.defaultBackupDir;
		// report error if still missing
		if (!storeUrl) {
			alert(config.macros.upload.messages.noStoreUrl);
			clearMessage();
			return false;
		}
		if (config.macros.upload.authenticateUser && (!username || !password)) {
			alert(config.macros.upload.messages.usernameOrPasswordMissing);
			clearMessage();
			return false;
		}
		bidix.upload.uploadChanges(false,null,storeUrl, toFilename, uploadDir, backupDir, username, password); 
		return false; 
};

config.macros.upload.destFile = function(storeUrl, toFilename, uploadDir) 
{
	if (!storeUrl)
		return null;
		var dest = bidix.dirname(storeUrl);
		if (uploadDir && uploadDir != '.')
			dest = dest + '/' + uploadDir;
		dest = dest + '/' + toFilename;
	return dest;
};

//
// uploadOptions Macro
//

config.macros.uploadOptions = {
	handler: function(place,macroName,params) {
		var wizard = new Wizard();
		wizard.createWizard(place,this.wizardTitle);
		wizard.addStep(this.step1Title,this.step1Html);
		var markList = wizard.getElement("markList");
		var listWrapper = document.createElement("div");
		markList.parentNode.insertBefore(listWrapper,markList);
		wizard.setValue("listWrapper",listWrapper);
		this.refreshOptions(listWrapper,false);
		var uploadCaption;
		if (document.location.toString().substr(0,4) == "http") 
			uploadCaption = config.macros.upload.label.saveLabel;
		else
			uploadCaption = config.macros.upload.label.uploadLabel;
		
		wizard.setButtons([
				{caption: uploadCaption, tooltip: config.macros.upload.label.promptOption, 
					onClick: config.macros.upload.action},
				{caption: this.cancelButton, tooltip: this.cancelButtonPrompt, onClick: this.onCancel}
				
			]);
	},
	options: [
		"txtUploadUserName",
		"pasUploadPassword",
		"txtUploadStoreUrl",
		"txtUploadDir",
		"txtUploadFilename",
		"txtUploadBackupDir",
		"chkUploadLog",
		"txtUploadLogMaxLine"		
	],
	refreshOptions: function(listWrapper) {
		var opts = [];
		for(i=0; i<this.options.length; i++) {
			var opt = {};
			opts.push();
			opt.option = "";
			n = this.options[i];
			opt.name = n;
			opt.lowlight = !config.optionsDesc[n];
			opt.description = opt.lowlight ? this.unknownDescription : config.optionsDesc[n];
			opts.push(opt);
		}
		var listview = ListView.create(listWrapper,opts,this.listViewTemplate);
		for(n=0; n<opts.length; n++) {
			var type = opts[n].name.substr(0,3);
			var h = config.macros.option.types[type];
			if (h && h.create) {
				h.create(opts[n].colElements['option'],type,opts[n].name,opts[n].name,"no");
			}
		}
		
	},
	onCancel: function(e)
	{
		backstage.switchTab(null);
		return false;
	},
	
	wizardTitle: "Upload with options",
	step1Title: "These options are saved in cookies in your browser",
	step1Html: "<input type='hidden' name='markList'></input><br>",
	cancelButton: "Cancel",
	cancelButtonPrompt: "Cancel prompt",
	listViewTemplate: {
		columns: [
			{name: 'Description', field: 'description', title: "Description", type: 'WikiText'},
			{name: 'Option', field: 'option', title: "Option", type: 'String'},
			{name: 'Name', field: 'name', title: "Name", type: 'String'}
			],
		rowClasses: [
			{className: 'lowlight', field: 'lowlight'} 
			]}
};

//
// upload functions
//

if (!bidix.upload) bidix.upload = {};

if (!bidix.upload.messages) bidix.upload.messages = {
	//from saving
	invalidFileError: "The original file '%0' does not appear to be a valid TiddlyWiki",
	backupSaved: "Backup saved",
	backupFailed: "Failed to upload backup file",
	rssSaved: "RSS feed uploaded",
	rssFailed: "Failed to upload RSS feed file",
	emptySaved: "Empty template uploaded",
	emptyFailed: "Failed to upload empty template file",
	mainSaved: "Main TiddlyWiki file uploaded",
	mainFailed: "Failed to upload main TiddlyWiki file. Your changes have not been saved",
	//specific upload
	loadOriginalHttpPostError: "Can't get original file",
	aboutToSaveOnHttpPost: 'About to upload on %0 ...',
	storePhpNotFound: "The store script '%0' was not found."
};

bidix.upload.uploadChanges = function(onlyIfDirty,tiddlers,storeUrl,toFilename,uploadDir,backupDir,username,password)
{
	var callback = function(status,uploadParams,original,url,xhr) {
		if (!status) {
			displayMessage(bidix.upload.messages.loadOriginalHttpPostError);
			return;
		}
		if (bidix.debugMode) 
			alert(original.substr(0,500)+"\n...");
		// Locate the storeArea div's 
		var posDiv = locateStoreArea(original);
		if((posDiv[0] == -1) || (posDiv[1] == -1)) {
			alert(config.messages.invalidFileError.format([localPath]));
			return;
		}
		bidix.upload.uploadRss(uploadParams,original,posDiv);
	};
	
	if(onlyIfDirty && !store.isDirty())
		return;
	clearMessage();
	// save on localdisk ?
	if (document.location.toString().substr(0,4) == "file") {
		var path = document.location.toString();
		var localPath = getLocalPath(path);
		saveChanges();
	}
	// get original
	var uploadParams = new Array(storeUrl,toFilename,uploadDir,backupDir,username,password);
	var originalPath = document.location.toString();
	// If url is a directory : add index.html
	if (originalPath.charAt(originalPath.length-1) == "/")
		originalPath = originalPath + "index.html";
	var dest = config.macros.upload.destFile(storeUrl,toFilename,uploadDir);
	var log = new bidix.UploadLog();
	log.startUpload(storeUrl, dest, uploadDir,  backupDir);
	displayMessage(bidix.upload.messages.aboutToSaveOnHttpPost.format([dest]));
	if (bidix.debugMode) 
		alert("about to execute Http - GET on "+originalPath);
	var r = doHttp("GET",originalPath,null,null,username,password,callback,uploadParams,null);
	if (typeof r == "string")
		displayMessage(r);
	return r;
};

bidix.upload.uploadRss = function(uploadParams,original,posDiv) 
{
	var callback = function(status,params,responseText,url,xhr) {
		if(status) {
			var destfile = responseText.substring(responseText.indexOf("destfile:")+9,responseText.indexOf("\n", responseText.indexOf("destfile:")));
			displayMessage(bidix.upload.messages.rssSaved,bidix.dirname(url)+'/'+destfile);
			bidix.upload.uploadMain(params[0],params[1],params[2]);
		} else {
			displayMessage(bidix.upload.messages.rssFailed);			
		}
	};
	// do uploadRss
	if(config.options.chkGenerateAnRssFeed) {
		var rssPath = uploadParams[1].substr(0,uploadParams[1].lastIndexOf(".")) + ".xml";
		var rssUploadParams = new Array(uploadParams[0],rssPath,uploadParams[2],'',uploadParams[4],uploadParams[5]);
		var rssString = generateRss();
		// no UnicodeToUTF8 conversion needed when location is "file" !!!
		if (document.location.toString().substr(0,4) != "file")
			rssString = convertUnicodeToUTF8(rssString);	
		bidix.upload.httpUpload(rssUploadParams,rssString,callback,Array(uploadParams,original,posDiv));
	} else {
		bidix.upload.uploadMain(uploadParams,original,posDiv);
	}
};

bidix.upload.uploadMain = function(uploadParams,original,posDiv) 
{
	var callback = function(status,params,responseText,url,xhr) {
		var log = new bidix.UploadLog();
		if(status) {
			// if backupDir specified
			if ((params[3]) && (responseText.indexOf("backupfile:") > -1))  {
				var backupfile = responseText.substring(responseText.indexOf("backupfile:")+11,responseText.indexOf("\n", responseText.indexOf("backupfile:")));
				displayMessage(bidix.upload.messages.backupSaved,bidix.dirname(url)+'/'+backupfile);
			}
			var destfile = responseText.substring(responseText.indexOf("destfile:")+9,responseText.indexOf("\n", responseText.indexOf("destfile:")));
			displayMessage(bidix.upload.messages.mainSaved,bidix.dirname(url)+'/'+destfile);
			store.setDirty(false);
			log.endUpload("ok");
		} else {
			alert(bidix.upload.messages.mainFailed);
			displayMessage(bidix.upload.messages.mainFailed);
			log.endUpload("failed");			
		}
	};
	// do uploadMain
	var revised = bidix.upload.updateOriginal(original,posDiv);
	bidix.upload.httpUpload(uploadParams,revised,callback,uploadParams);
};

bidix.upload.httpUpload = function(uploadParams,data,callback,params)
{
	var localCallback = function(status,params,responseText,url,xhr) {
		url = (url.indexOf("nocache=") < 0 ? url : url.substring(0,url.indexOf("nocache=")-1));
		if (xhr.status == 404)
			alert(bidix.upload.messages.storePhpNotFound.format([url]));
		if ((bidix.debugMode) || (responseText.indexOf("Debug mode") >= 0 )) {
			alert(responseText);
			if (responseText.indexOf("Debug mode") >= 0 )
				responseText = responseText.substring(responseText.indexOf("\n\n")+2);
		} else if (responseText.charAt(0) != '0') 
			alert(responseText);
		if (responseText.charAt(0) != '0')
			status = null;
		callback(status,params,responseText,url,xhr);
	};
	// do httpUpload
	var boundary = "---------------------------"+"AaB03x";	
	var uploadFormName = "UploadPlugin";
	// compose headers data
	var sheader = "";
	sheader += "--" + boundary + "\r\nContent-disposition: form-data; name=\"";
	sheader += uploadFormName +"\"\r\n\r\n";
	sheader += "backupDir="+uploadParams[3] +
				";user=" + uploadParams[4] +
				";password=" + uploadParams[5] +
				";uploaddir=" + uploadParams[2];
	if (bidix.debugMode)
		sheader += ";debug=1";
	sheader += ";;\r\n"; 
	sheader += "\r\n" + "--" + boundary + "\r\n";
	sheader += "Content-disposition: form-data; name=\"userfile\"; filename=\""+uploadParams[1]+"\"\r\n";
	sheader += "Content-Type: text/html;charset=UTF-8" + "\r\n";
	sheader += "Content-Length: " + data.length + "\r\n\r\n";
	// compose trailer data
	var strailer = new String();
	strailer = "\r\n--" + boundary + "--\r\n";
	data = sheader + data + strailer;
	if (bidix.debugMode) alert("about to execute Http - POST on "+uploadParams[0]+"\n with \n"+data.substr(0,500)+ " ... ");
	var r = doHttp("POST",uploadParams[0],data,"multipart/form-data; ;charset=UTF-8; boundary="+boundary,uploadParams[4],uploadParams[5],localCallback,params,null);
	if (typeof r == "string")
		displayMessage(r);
	return r;
};

// same as Saving's updateOriginal but without convertUnicodeToUTF8 calls
bidix.upload.updateOriginal = function(original, posDiv)
{
	if (!posDiv)
		posDiv = locateStoreArea(original);
	if((posDiv[0] == -1) || (posDiv[1] == -1)) {
		alert(config.messages.invalidFileError.format([localPath]));
		return;
	}
	var revised = original.substr(0,posDiv[0] + startSaveArea.length) + "\n" +
				store.allTiddlersAsHtml() + "\n" +
				original.substr(posDiv[1]);
	var newSiteTitle = getPageTitle().htmlEncode();
	revised = revised.replaceChunk("<title"+">","</title"+">"," " + newSiteTitle + " ");
	revised = updateMarkupBlock(revised,"PRE-HEAD","MarkupPreHead");
	revised = updateMarkupBlock(revised,"POST-HEAD","MarkupPostHead");
	revised = updateMarkupBlock(revised,"PRE-BODY","MarkupPreBody");
	revised = updateMarkupBlock(revised,"POST-SCRIPT","MarkupPostBody");
	return revised;
};

//
// UploadLog
// 
// config.options.chkUploadLog :
//		false : no logging
//		true : logging
// config.options.txtUploadLogMaxLine :
//		-1 : no limit
//      0 :  no Log lines but UploadLog is still in place
//		n :  the last n lines are only kept
//		NaN : no limit (-1)

bidix.UploadLog = function() {
	if (!config.options.chkUploadLog) 
		return; // this.tiddler = null
	this.tiddler = store.getTiddler("UploadLog");
	if (!this.tiddler) {
		this.tiddler = new Tiddler();
		this.tiddler.title = "UploadLog";
		this.tiddler.text = "| !date | !user | !location | !storeUrl | !uploadDir | !toFilename | !backupdir | !origin |";
		this.tiddler.created = new Date();
		this.tiddler.modifier = config.options.txtUserName;
		this.tiddler.modified = new Date();
		store.addTiddler(this.tiddler);
	}
	return this;
};

bidix.UploadLog.prototype.addText = function(text) {
	if (!this.tiddler)
		return;
	// retrieve maxLine when we need it
	var maxLine = parseInt(config.options.txtUploadLogMaxLine,10);
	if (isNaN(maxLine))
		maxLine = -1;
	// add text
	if (maxLine != 0) 
		this.tiddler.text = this.tiddler.text + text;
	// Trunck to maxLine
	if (maxLine >= 0) {
		var textArray = this.tiddler.text.split('\n');
		if (textArray.length > maxLine + 1)
			textArray.splice(1,textArray.length-1-maxLine);
			this.tiddler.text = textArray.join('\n');		
	}
	// update tiddler fields
	this.tiddler.modifier = config.options.txtUserName;
	this.tiddler.modified = new Date();
	store.addTiddler(this.tiddler);
	// refresh and notifiy for immediate update
	story.refreshTiddler(this.tiddler.title);
	store.notify(this.tiddler.title, true);
};

bidix.UploadLog.prototype.startUpload = function(storeUrl, toFilename, uploadDir,  backupDir) {
	if (!this.tiddler)
		return;
	var now = new Date();
	var text = "\n| ";
	var filename = bidix.basename(document.location.toString());
	if (!filename) filename = '/';
	text += now.formatString("0DD/0MM/YYYY 0hh:0mm:0ss") +" | ";
	text += config.options.txtUserName + " | ";
	text += "[["+filename+"|"+location + "]] |";
	text += " [[" + bidix.basename(storeUrl) + "|" + storeUrl + "]] | ";
	text += uploadDir + " | ";
	text += "[[" + bidix.basename(toFilename) + " | " +toFilename + "]] | ";
	text += backupDir + " |";
	this.addText(text);
};

bidix.UploadLog.prototype.endUpload = function(status) {
	if (!this.tiddler)
		return;
	this.addText(" "+status+" |");
};

//
// Utilities
// 

bidix.checkPlugin = function(plugin, major, minor, revision) {
	var ext = version.extensions[plugin];
	if (!
		(ext  && 
			((ext.major > major) || 
			((ext.major == major) && (ext.minor > minor))  ||
			((ext.major == major) && (ext.minor == minor) && (ext.revision >= revision))))) {
			// write error in PluginManager
			if (pluginInfo)
				pluginInfo.log.push("Requires " + plugin + " " + major + "." + minor + "." + revision);
			eval(plugin); // generate an error : "Error: ReferenceError: xxxx is not defined"
	}
};

bidix.dirname = function(filePath) {
	if (!filePath) 
		return;
	var lastpos;
	if ((lastpos = filePath.lastIndexOf("/")) != -1) {
		return filePath.substring(0, lastpos);
	} else {
		return filePath.substring(0, filePath.lastIndexOf("\\"));
	}
};

bidix.basename = function(filePath) {
	if (!filePath) 
		return;
	var lastpos;
	if ((lastpos = filePath.lastIndexOf("#")) != -1) 
		filePath = filePath.substring(0, lastpos);
	if ((lastpos = filePath.lastIndexOf("/")) != -1) {
		return filePath.substring(lastpos + 1);
	} else
		return filePath.substring(filePath.lastIndexOf("\\")+1);
};

bidix.initOption = function(name,value) {
	if (!config.options[name])
		config.options[name] = value;
};

//
// Initializations
//

// require PasswordOptionPlugin 1.0.1 or better
bidix.checkPlugin("PasswordOptionPlugin", 1, 0, 1);

// styleSheet
setStylesheet('.txtUploadStoreUrl, .txtUploadBackupDir, .txtUploadDir {width: 22em;}',"uploadPluginStyles");

//optionsDesc
merge(config.optionsDesc,{
	txtUploadStoreUrl: "Url of the UploadService script (default: store.php)",
	txtUploadFilename: "Filename of the uploaded file (default: in index.html)",
	txtUploadDir: "Relative Directory where to store the file (default: . (downloadService directory))",
	txtUploadBackupDir: "Relative Directory where to backup the file. If empty no backup. (default: ''(empty))",
	txtUploadUserName: "Upload Username",
	pasUploadPassword: "Upload Password",
	chkUploadLog: "do Logging in UploadLog (default: true)",
	txtUploadLogMaxLine: "Maximum of lines in UploadLog (default: 10)"
});

// Options Initializations
bidix.initOption('txtUploadStoreUrl','');
bidix.initOption('txtUploadFilename','');
bidix.initOption('txtUploadDir','');
bidix.initOption('txtUploadBackupDir','');
bidix.initOption('txtUploadUserName','');
bidix.initOption('pasUploadPassword','');
bidix.initOption('chkUploadLog',true);
bidix.initOption('txtUploadLogMaxLine','10');


// Backstage
merge(config.tasks,{
	uploadOptions: {text: "upload", tooltip: "Change UploadOptions and Upload", content: '<<uploadOptions>>'}
});
config.backstageTasks.push("uploadOptions");


//}}}

!!This Tiddlyspot was set up to demonstrate the ~CHAPTimelinePlugin.
The following tiddlers are of relevance:
*Sample setup
**[[SampleTimeline using one Tag and standard slice names]]
***[[Event 1]]
***[[Event 2]]
***[[Event 3]]
***[[Event 4]]
***[[Event 5]]

**[[SampleTimeline using ForEachTiddler plugin]]
***[[Event A]]
***[[Event B]]

*Backend/Plugin/Configuration
**[[CHAPTimelinePlugin]]
**CHAPTimelineCSS
**StyleSheet
**DataTiddlerPlugin
**ForEachTiddlerPlugin


This document is a ~TiddlyWiki from tiddlyspot.com.  A ~TiddlyWiki is an electronic notebook that is great for managing todo lists, personal information, and all sorts of things.

@@font-weight:bold;font-size:1.3em;color:#444; //What now?// &nbsp;&nbsp;@@ Before you can save any changes, you need to enter your password in the form below.  Then configure privacy and other site settings at your [[control panel|http://chaptimeline.tiddlyspot.com/controlpanel]] (your control panel username is //chaptimeline//).
<<tiddler TspotControls>>
See also GettingStarted.

@@font-weight:bold;font-size:1.3em;color:#444; //Working online// &nbsp;&nbsp;@@ You can edit this ~TiddlyWiki right now, and save your changes using the "save to web" button in the column on the right.

@@font-weight:bold;font-size:1.3em;color:#444; //Working offline// &nbsp;&nbsp;@@ A fully functioning copy of this ~TiddlyWiki can be saved onto your hard drive or USB stick.  You can make changes and save them locally without being connected to the Internet.  When you're ready to sync up again, just click "upload" and your ~TiddlyWiki will be saved back to tiddlyspot.com.

@@font-weight:bold;font-size:1.3em;color:#444; //Help!// &nbsp;&nbsp;@@ Find out more about ~TiddlyWiki at [[TiddlyWiki.com|http://tiddlywiki.com]].  Also visit [[TiddlyWiki.org|http://tiddlywiki.org]] for documentation on learning and using ~TiddlyWiki. New users are especially welcome on the [[TiddlyWiki mailing list|http://groups.google.com/group/TiddlyWiki]], which is an excellent place to ask questions and get help.  If you have a tiddlyspot related problem email [[tiddlyspot support|mailto:support@tiddlyspot.com]].

@@font-weight:bold;font-size:1.3em;color:#444; //Enjoy :)// &nbsp;&nbsp;@@ We hope you like using your tiddlyspot.com site.  Please email [[feedback@tiddlyspot.com|mailto:feedback@tiddlyspot.com]] with any comments or suggestions.

<script>
cid = "144921";
</script>
<script src="http://www.ezwebsitecounter.com/c.js?id=144921"></script>