timeline.init.js 164 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527252825292530253125322533253425352536253725382539254025412542254325442545254625472548254925502551255225532554255525562557255825592560256125622563256425652566256725682569257025712572257325742575257625772578257925802581258225832584258525862587258825892590259125922593259425952596259725982599260026012602260326042605260626072608260926102611261226132614261526162617261826192620262126222623262426252626262726282629263026312632263326342635263626372638263926402641264226432644264526462647264826492650265126522653265426552656265726582659266026612662266326642665266626672668266926702671267226732674267526762677267826792680268126822683268426852686268726882689269026912692269326942695269626972698269927002701270227032704270527062707270827092710271127122713271427152716271727182719272027212722272327242725272627272728272927302731273227332734273527362737273827392740274127422743274427452746274727482749275027512752275327542755275627572758275927602761276227632764276527662767276827692770277127722773277427752776277727782779278027812782278327842785278627872788278927902791279227932794279527962797279827992800280128022803280428052806280728082809281028112812281328142815281628172818281928202821282228232824282528262827282828292830283128322833283428352836283728382839284028412842284328442845284628472848284928502851285228532854285528562857285828592860286128622863286428652866286728682869287028712872287328742875287628772878287928802881288228832884288528862887288828892890289128922893289428952896289728982899290029012902290329042905290629072908290929102911291229132914291529162917291829192920292129222923292429252926292729282929293029312932293329342935293629372938293929402941294229432944294529462947294829492950295129522953295429552956295729582959296029612962296329642965296629672968296929702971297229732974297529762977297829792980298129822983298429852986298729882989299029912992299329942995299629972998299930003001300230033004300530063007300830093010301130123013301430153016301730183019302030213022302330243025302630273028302930303031303230333034303530363037303830393040304130423043304430453046304730483049305030513052305330543055305630573058305930603061306230633064306530663067306830693070307130723073307430753076307730783079308030813082308330843085308630873088308930903091309230933094309530963097309830993100310131023103310431053106310731083109311031113112311331143115311631173118311931203121312231233124312531263127312831293130313131323133313431353136313731383139314031413142314331443145314631473148314931503151315231533154315531563157315831593160316131623163316431653166316731683169317031713172317331743175317631773178317931803181318231833184318531863187318831893190319131923193319431953196319731983199320032013202320332043205320632073208320932103211321232133214321532163217321832193220322132223223322432253226322732283229323032313232323332343235323632373238323932403241324232433244324532463247324832493250325132523253325432553256325732583259326032613262326332643265326632673268326932703271327232733274327532763277327832793280328132823283328432853286328732883289329032913292329332943295329632973298329933003301330233033304330533063307330833093310331133123313331433153316331733183319332033213322332333243325332633273328332933303331333233333334333533363337333833393340334133423343334433453346334733483349335033513352335333543355335633573358335933603361336233633364336533663367336833693370337133723373337433753376337733783379338033813382338333843385338633873388338933903391339233933394339533963397339833993400340134023403340434053406340734083409341034113412341334143415341634173418341934203421342234233424342534263427342834293430343134323433343434353436343734383439344034413442344334443445344634473448344934503451345234533454345534563457345834593460346134623463346434653466346734683469347034713472347334743475347634773478347934803481348234833484348534863487348834893490349134923493349434953496349734983499350035013502350335043505350635073508350935103511351235133514351535163517351835193520352135223523352435253526352735283529353035313532353335343535353635373538353935403541354235433544354535463547354835493550355135523553355435553556355735583559356035613562356335643565356635673568356935703571357235733574357535763577357835793580358135823583358435853586358735883589359035913592359335943595359635973598359936003601360236033604360536063607360836093610361136123613361436153616361736183619362036213622362336243625362636273628362936303631363236333634363536363637363836393640364136423643364436453646364736483649365036513652365336543655365636573658365936603661366236633664366536663667366836693670367136723673367436753676367736783679368036813682368336843685368636873688368936903691369236933694369536963697369836993700370137023703370437053706370737083709371037113712371337143715371637173718371937203721372237233724372537263727372837293730373137323733373437353736373737383739374037413742374337443745374637473748374937503751375237533754375537563757375837593760376137623763376437653766376737683769377037713772377337743775377637773778377937803781378237833784378537863787378837893790379137923793379437953796379737983799380038013802380338043805380638073808380938103811381238133814381538163817381838193820382138223823382438253826382738283829383038313832383338343835383638373838383938403841384238433844384538463847384838493850385138523853385438553856385738583859386038613862386338643865386638673868386938703871387238733874387538763877387838793880388138823883388438853886388738883889389038913892389338943895389638973898389939003901390239033904390539063907390839093910391139123913391439153916391739183919392039213922392339243925392639273928392939303931393239333934393539363937393839393940394139423943394439453946394739483949395039513952395339543955395639573958395939603961396239633964396539663967396839693970397139723973397439753976397739783979398039813982398339843985398639873988398939903991399239933994399539963997399839994000400140024003400440054006400740084009401040114012401340144015401640174018401940204021402240234024402540264027402840294030403140324033403440354036403740384039404040414042404340444045404640474048404940504051405240534054405540564057405840594060406140624063406440654066406740684069407040714072407340744075407640774078407940804081408240834084408540864087408840894090409140924093409440954096409740984099410041014102410341044105410641074108410941104111411241134114411541164117411841194120412141224123412441254126412741284129413041314132413341344135413641374138413941404141414241434144414541464147414841494150415141524153415441554156415741584159416041614162416341644165416641674168416941704171417241734174417541764177417841794180418141824183418441854186418741884189419041914192419341944195419641974198419942004201420242034204420542064207420842094210421142124213421442154216421742184219422042214222422342244225422642274228422942304231423242334234423542364237423842394240424142424243424442454246424742484249425042514252425342544255425642574258425942604261426242634264426542664267426842694270427142724273427442754276427742784279428042814282428342844285428642874288428942904291429242934294429542964297429842994300430143024303430443054306430743084309431043114312431343144315431643174318431943204321432243234324432543264327432843294330433143324333433443354336433743384339434043414342434343444345434643474348434943504351435243534354435543564357435843594360436143624363436443654366436743684369437043714372437343744375437643774378437943804381438243834384438543864387438843894390439143924393439443954396439743984399440044014402440344044405440644074408440944104411441244134414441544164417441844194420442144224423442444254426442744284429443044314432443344344435443644374438443944404441444244434444444544464447444844494450445144524453445444554456445744584459446044614462446344644465446644674468446944704471447244734474447544764477447844794480448144824483448444854486448744884489449044914492449344944495449644974498449945004501450245034504450545064507450845094510451145124513451445154516451745184519452045214522452345244525452645274528452945304531453245334534453545364537453845394540454145424543454445454546454745484549455045514552455345544555455645574558455945604561456245634564456545664567456845694570457145724573457445754576457745784579458045814582458345844585458645874588458945904591459245934594459545964597459845994600460146024603460446054606460746084609461046114612461346144615461646174618461946204621462246234624462546264627462846294630463146324633463446354636463746384639464046414642464346444645464646474648464946504651465246534654465546564657465846594660466146624663466446654666466746684669467046714672467346744675467646774678467946804681468246834684468546864687468846894690469146924693469446954696469746984699470047014702470347044705470647074708470947104711471247134714471547164717471847194720472147224723472447254726472747284729473047314732473347344735473647374738473947404741474247434744474547464747474847494750475147524753475447554756475747584759476047614762476347644765476647674768476947704771477247734774477547764777477847794780478147824783478447854786478747884789479047914792479347944795479647974798479948004801480248034804480548064807480848094810481148124813481448154816481748184819482048214822482348244825482648274828482948304831483248334834483548364837483848394840484148424843484448454846484748484849485048514852485348544855485648574858485948604861486248634864486548664867486848694870487148724873487448754876487748784879488048814882488348844885488648874888488948904891489248934894489548964897489848994900490149024903490449054906490749084909491049114912491349144915491649174918491949204921492249234924492549264927492849294930493149324933493449354936493749384939494049414942494349444945494649474948494949504951495249534954495549564957495849594960496149624963496449654966496749684969497049714972497349744975497649774978497949804981498249834984498549864987498849894990499149924993499449954996499749984999500050015002500350045005500650075008500950105011501250135014501550165017501850195020502150225023502450255026
  1. /**
  2. * @file timeline.js
  3. *
  4. * @brief
  5. * The Timeline is an interactive visualization chart to visualize events in
  6. * time, having a start and end date.
  7. * You can freely move and zoom in the timeline by dragging
  8. * and scrolling in the Timeline. Items are optionally dragable. The time
  9. * scale on the axis is adjusted automatically, and supports scales ranging
  10. * from milliseconds to years.
  11. *
  12. * Timeline is part of the CHAP Links library.
  13. *
  14. * Timeline is tested on Firefox 3.6, Safari 5.0, Chrome 6.0, Opera 10.6, and
  15. * Internet Explorer 6+.
  16. *
  17. * @license
  18. * Licensed under the Apache License, Version 2.0 (the "License"); you may not
  19. * use this file except in compliance with the License. You may obtain a copy
  20. * of the License at
  21. *
  22. * http://www.apache.org/licenses/LICENSE-2.0
  23. *
  24. * Unless required by applicable law or agreed to in writing, software
  25. * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
  26. * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
  27. * License for the specific language governing permissions and limitations under
  28. * the License.
  29. *
  30. * Copyright (c) 2011-2012 Almende B.V.
  31. *
  32. * @author Jos de Jong, <jos@almende.org>
  33. * @date 2012-06-15
  34. * @version 2.1.2
  35. */
  36. /*
  37. * TODO
  38. *
  39. * Add zooming with pinching on Android
  40. *
  41. * Bug: when an item contains a javascript onclick or a link, this does not work
  42. * when the item is not selected (when the item is being selected,
  43. * it is redrawn, which cancels any onclick or link action)
  44. * Bug: when an item contains an image without size, or a css max-width, it is not sized correctly
  45. * Bug: neglect items when they have no valid start/end, instead of throwing an error
  46. * Bug: Pinching on ipad does not work very well, sometimes the page will zoom when pinching vertically
  47. * Bug: cannot set max width for an item, like div.timeline-event-content {white-space: normal; max-width: 100px;}
  48. * Bug on IE in Quirks mode. When you have groups, and delete an item, the groups become invisible
  49. */
  50. /**
  51. * Declare a unique namespace for CHAP's Common Hybrid Visualisation Library,
  52. * "links"
  53. */
  54. if (typeof links === 'undefined') {
  55. links = {};
  56. // important: do not use var, as "var links = {};" will overwrite
  57. // the existing links variable value with undefined in IE8, IE7.
  58. }
  59. /**
  60. * Ensure the variable google exists
  61. */
  62. if (typeof google === 'undefined') {
  63. google = undefined;
  64. // important: do not use var, as "var google = undefined;" will overwrite
  65. // the existing google variable value with undefined in IE8, IE7.
  66. }
  67. /**
  68. * @constructor links.Timeline
  69. * The timeline is a visualization chart to visualize events in time.
  70. *
  71. * The timeline is developed in javascript as a Google Visualization Chart.
  72. *
  73. * @param {Element} container The DOM element in which the Timeline will
  74. * be created. Normally a div element.
  75. */
  76. links.Timeline = function(container) {
  77. // create variables and set default values
  78. this.dom = {};
  79. this.conversion = {};
  80. this.eventParams = {}; // stores parameters for mouse events
  81. this.groups = [];
  82. this.groupIndexes = {};
  83. this.items = [];
  84. this.selection = undefined; // stores index and item which is currently selected
  85. this.listeners = {}; // event listener callbacks
  86. // Initialize sizes.
  87. // Needed for IE (which gives an error when you try to set an undefined
  88. // value in a style)
  89. this.size = {
  90. 'actualHeight': 0,
  91. 'axis': {
  92. 'characterMajorHeight': 0,
  93. 'characterMajorWidth': 0,
  94. 'characterMinorHeight': 0,
  95. 'characterMinorWidth': 0,
  96. 'height': 0,
  97. 'labelMajorTop': 0,
  98. 'labelMinorTop': 0,
  99. 'line': 0,
  100. 'lineMajorWidth': 0,
  101. 'lineMinorHeight': 0,
  102. 'lineMinorTop': 0,
  103. 'lineMinorWidth': 0,
  104. 'top': 0
  105. },
  106. 'contentHeight': 0,
  107. 'contentLeft': 0,
  108. 'contentWidth': 0,
  109. 'dataChanged': false,
  110. 'frameHeight': 0,
  111. 'frameWidth': 0,
  112. 'groupsLeft': 0,
  113. 'groupsWidth': 0,
  114. 'items': {
  115. 'top': 0
  116. }
  117. };
  118. this.dom.container = container;
  119. this.options = {
  120. 'width': "100%",
  121. 'height': "auto",
  122. 'minHeight': 0, // minimal height in pixels
  123. 'autoHeight': true,
  124. 'eventMargin': 10, // minimal margin between events
  125. 'eventMarginAxis': 20, // minimal margin beteen events and the axis
  126. 'dragAreaWidth': 10, // pixels
  127. 'min': undefined,
  128. 'max': undefined,
  129. 'intervalMin': 10, // milliseconds
  130. 'intervalMax': 1000 * 60 * 60 * 24 * 365 * 10000, // milliseconds
  131. 'moveable': true,
  132. 'zoomable': true,
  133. 'selectable': true,
  134. 'editable': false,
  135. 'snapEvents': true,
  136. 'groupChangeable': true,
  137. 'showCurrentTime': true, // show a red bar displaying the current time
  138. 'showCustomTime': false, // show a blue, draggable bar displaying a custom time
  139. 'showMajorLabels': true,
  140. 'showNavigation': false,
  141. 'showButtonAdd': true,
  142. 'groupsOnRight': false,
  143. 'axisOnTop': false,
  144. 'stackEvents': true,
  145. 'animate': true,
  146. 'animateZoom': true,
  147. 'style': 'box'
  148. };
  149. this.clientTimeOffset = 0; // difference between client time and the time
  150. // set via Timeline.setCurrentTime()
  151. var dom = this.dom;
  152. // remove all elements from the container element.
  153. while (dom.container.hasChildNodes()) {
  154. dom.container.removeChild(dom.container.firstChild);
  155. }
  156. // create a step for drawing the axis
  157. this.step = new links.Timeline.StepDate();
  158. // initialize data
  159. this.data = [];
  160. this.firstDraw = true;
  161. // date interval must be initialized
  162. this.setVisibleChartRange(undefined, undefined, false);
  163. // create all DOM elements
  164. this.redrawFrame();
  165. // Internet Explorer does not support Array.indexof,
  166. // so we define it here in that case
  167. // http://soledadpenades.com/2007/05/17/arrayindexof-in-internet-explorer/
  168. if(!Array.prototype.indexOf) {
  169. Array.prototype.indexOf = function(obj){
  170. for(var i = 0; i < this.length; i++){
  171. if(this[i] == obj){
  172. return i;
  173. }
  174. }
  175. return -1;
  176. }
  177. }
  178. // fire the ready event
  179. this.trigger('ready');
  180. };
  181. /**
  182. * Main drawing logic. This is the function that needs to be called
  183. * in the html page, to draw the timeline.
  184. *
  185. * A data table with the events must be provided, and an options table.
  186. *
  187. * @param {google.visualization.DataTable} data
  188. * The data containing the events for the timeline.
  189. * Object DataTable is defined in
  190. * google.visualization.DataTable
  191. * @param {Object} options A name/value map containing settings for the
  192. * timeline. Optional.
  193. */
  194. links.Timeline.prototype.draw = function(data, options) {
  195. this.setOptions(options);
  196. // read the data
  197. this.setData(data);
  198. // set timer range. this will also redraw the timeline
  199. if (options && options.start && options.end) {
  200. this.setVisibleChartRange(options.start, options.end);
  201. }
  202. else if (this.firstDraw) {
  203. this.setVisibleChartRangeAuto();
  204. }
  205. this.firstDraw = false;
  206. };
  207. /**
  208. * Set options for the timeline.
  209. * Timeline must be redrawn afterwards
  210. * @param {Object} options A name/value map containing settings for the
  211. * timeline. Optional.
  212. */
  213. links.Timeline.prototype.setOptions = function(options) {
  214. if (options) {
  215. // retrieve parameter values
  216. for (var i in options) {
  217. if (options.hasOwnProperty(i)) {
  218. this.options[i] = options[i];
  219. }
  220. }
  221. }
  222. // validate options
  223. this.options.autoHeight = (this.options.height === "auto");
  224. };
  225. /**
  226. * Set data for the timeline
  227. * @param {google.visualization.DataTable | array} data
  228. */
  229. links.Timeline.prototype.setData = function(data) {
  230. // unselect any previously selected item
  231. this.unselectItem();
  232. if (!data) {
  233. data = [];
  234. }
  235. this.items = [];
  236. this.data = data;
  237. var items = this.items;
  238. var options = this.options;
  239. // create groups from the data
  240. this.setGroups(data);
  241. if (google && google.visualization &&
  242. data instanceof google.visualization.DataTable) {
  243. // read DataTable
  244. var hasGroups = (data.getNumberOfColumns() > 3);
  245. for (var row = 0, rows = data.getNumberOfRows(); row < rows; row++) {
  246. items.push(this.createItem({
  247. 'start': data.getValue(row, 0),
  248. 'end': data.getValue(row, 1),
  249. 'content': data.getValue(row, 2),
  250. 'group': (hasGroups ? data.getValue(row, 3) : undefined)
  251. }));
  252. }
  253. }
  254. else if (links.Timeline.isArray(data)) {
  255. // read JSON array
  256. for (var row = 0, rows = data.length; row < rows; row++) {
  257. var itemData = data[row]
  258. var item = this.createItem(itemData);
  259. items.push(item);
  260. }
  261. }
  262. else {
  263. throw "Unknown data type. DataTable or Array expected.";
  264. }
  265. // set a flag to force the recalcSize method to recalculate the
  266. // heights and widths of the events
  267. this.size.dataChanged = true;
  268. this.redrawFrame(); // create the items for the new data
  269. this.recalcSize(); // position the items
  270. this.stackEvents(false);
  271. this.redrawFrame(); // redraw the items on the final positions
  272. this.size.dataChanged = false;
  273. };
  274. /**
  275. * Set the groups available in the given dataset
  276. * @param {google.visualization.DataTable | array} data
  277. */
  278. links.Timeline.prototype.setGroups = function (data) {
  279. this.deleteGroups();
  280. var groups = this.groups;
  281. var groupIndexes = this.groupIndexes;
  282. if (google && google.visualization &&
  283. data instanceof google.visualization.DataTable) {
  284. // get groups from DataTable
  285. var hasGroups = (data.getNumberOfColumns() > 3);
  286. if (hasGroups) {
  287. var groupNames = data.getDistinctValues(3);
  288. for (var i = 0, iMax = groupNames.length; i < iMax; i++) {
  289. this.addGroup(groupNames[i]);
  290. }
  291. }
  292. }
  293. else if (links.Timeline.isArray(data)){
  294. // get groups from JSON Array
  295. for (var i = 0, iMax = data.length; i < iMax; i++) {
  296. var row = data[i],
  297. group = row.group;
  298. if (group) {
  299. this.addGroup(group);
  300. }
  301. }
  302. }
  303. else {
  304. throw 'Unknown data type. DataTable or Array expected.';
  305. }
  306. };
  307. /**
  308. * Return the original data table.
  309. * @return {google.visualization.DataTable | Array} data
  310. */
  311. links.Timeline.prototype.getData = function () {
  312. return this.data;
  313. };
  314. /**
  315. * Update the original data with changed start, end or group.
  316. *
  317. * @param {Number} index
  318. * @param {Object} values An object containing some of the following parameters:
  319. * {Date} start,
  320. * {Date} end,
  321. * {String} content,
  322. * {String} group
  323. */
  324. links.Timeline.prototype.updateData = function (index, values) {
  325. var data = this.data;
  326. if (google && google.visualization &&
  327. data instanceof google.visualization.DataTable) {
  328. // update the original google DataTable
  329. var missingRows = (index + 1) - data.getNumberOfRows();
  330. if (missingRows > 0) {
  331. data.addRows(missingRows);
  332. }
  333. if (values.start) {
  334. data.setValue(index, 0, values.start);
  335. }
  336. if (values.end) {
  337. data.setValue(index, 1, values.end);
  338. }
  339. if (values.content) {
  340. data.setValue(index, 2, values.content);
  341. }
  342. if (values.group && data.getNumberOfColumns() > 3) {
  343. // TODO: append a column when needed?
  344. data.setValue(index, 3, values.group);
  345. }
  346. }
  347. else if (links.Timeline.isArray(data)) {
  348. // update the original JSON table
  349. var row = data[index];
  350. if (row == undefined) {
  351. row = {};
  352. data[index] = row;
  353. }
  354. if (values.start) {
  355. row.start = values.start;
  356. }
  357. if (values.end) {
  358. row.end = values.end;
  359. }
  360. if (values.content) {
  361. row.content = values.content;
  362. }
  363. if (values.group) {
  364. row.group = values.group;
  365. }
  366. }
  367. else {
  368. throw "Cannot update data, unknown type of data";
  369. }
  370. };
  371. /**
  372. * Find the item index from a given HTML element
  373. * If no item index is found, undefined is returned
  374. * @param {Element} element
  375. * @return {Number} index
  376. */
  377. links.Timeline.prototype.getItemIndex = function(element) {
  378. var e = element,
  379. dom = this.dom,
  380. items = this.items,
  381. index = undefined;
  382. // try to find the frame where the items are located in
  383. while (e.parentNode && e.parentNode !== dom.items.frame) {
  384. e = e.parentNode;
  385. }
  386. if (e.parentNode === dom.items.frame) {
  387. // yes! we have found the parent element of all items
  388. // retrieve its id from the array with items
  389. for (var i = 0, iMax = items.length; i < iMax; i++) {
  390. if (items[i].dom === e) {
  391. index = i;
  392. break;
  393. }
  394. }
  395. }
  396. return index;
  397. };
  398. /**
  399. * Set a new size for the timeline
  400. * @param {string} width Width in pixels or percentage (for example "800px"
  401. * or "50%")
  402. * @param {string} height Height in pixels or percentage (for example "400px"
  403. * or "30%")
  404. */
  405. links.Timeline.prototype.setSize = function(width, height) {
  406. if (width) {
  407. this.options.width = width;
  408. this.dom.frame.style.width = width;
  409. }
  410. if (height) {
  411. this.options.height = height;
  412. this.options.autoHeight = (this.options.height === "auto");
  413. if (height !== "auto" ) {
  414. this.dom.frame.style.height = height;
  415. }
  416. }
  417. this.recalcSize();
  418. this.stackEvents(false);
  419. this.redrawFrame();
  420. };
  421. /**
  422. * Set a new value for the visible range int the timeline.
  423. * Set start to null to include everything from the earliest date to end.
  424. * Set end to null to include everything from start to the last date.
  425. * Example usage:
  426. * myTimeline.setVisibleChartRange(new Date("2010-08-22"),
  427. * new Date("2010-09-13"));
  428. * @param {Date} start The start date for the timeline. optional
  429. * @param {Date} end The end date for the timeline. optional
  430. * @param {boolean} redraw Optional. If true (default) the Timeline is
  431. * directly redrawn
  432. */
  433. links.Timeline.prototype.setVisibleChartRange = function(start, end, redraw) {
  434. if (start == undefined) {
  435. // default of 3 days ago
  436. start = new Date();
  437. start.setDate(start.getDate() - 3);
  438. }
  439. if (end == undefined) {
  440. // default of 4 days ahead
  441. end = new Date();
  442. end.setDate(start.getDate() + 4);
  443. }
  444. // prevent start Date <= end Date
  445. if (end.valueOf() <= start.valueOf()) {
  446. end = new Date(start);
  447. end.setDate(end.getDate() + 7);
  448. }
  449. // limit to the allowed range (dont let this do by applyRange,
  450. // because that method will try to maintain the interval (end-start)
  451. var min = this.options.min ? this.options.min.valueOf() : undefined;
  452. if (min && start.valueOf() < min) {
  453. start = new Date(min);
  454. }
  455. var max = this.options.max ? this.options.max.valueOf() : undefined;
  456. if (max && end.valueOf() > max) {
  457. end = new Date(max);
  458. }
  459. this.applyRange(start, end);
  460. if (redraw == undefined || redraw == true) {
  461. this.recalcSize();
  462. this.stackEvents(false);
  463. this.redrawFrame();
  464. }
  465. else {
  466. this.recalcConversion();
  467. }
  468. };
  469. /**
  470. * Change the visible chart range such that all items become visible
  471. */
  472. links.Timeline.prototype.setVisibleChartRangeAuto = function() {
  473. var items = this.items,
  474. startMin = undefined, // long value of a data
  475. endMax = undefined; // long value of a data
  476. // find earliest start date from the data
  477. for (var i = 0, iMax = items.length; i < iMax; i++) {
  478. var item = items[i],
  479. start = item.start ? item.start.valueOf() : undefined,
  480. end = item.end ? item.end.valueOf() : start;
  481. if (startMin !== undefined && start !== undefined) {
  482. startMin = Math.min(startMin, start);
  483. }
  484. else {
  485. startMin = start;
  486. }
  487. if (endMax !== undefined && end !== undefined) {
  488. endMax = Math.max(endMax, end);
  489. }
  490. else {
  491. endMax = end;
  492. }
  493. }
  494. if (startMin !== undefined && endMax !== undefined) {
  495. // zoom out 5% such that you have a little white space on the left and right
  496. var center = (endMax + startMin) / 2,
  497. diff = (endMax - startMin);
  498. startMin = startMin - diff * 0.05;
  499. endMax = endMax + diff * 0.05;
  500. // adjust the start and end date
  501. this.setVisibleChartRange(new Date(startMin), new Date(endMax));
  502. }
  503. else {
  504. this.setVisibleChartRange(undefined, undefined);
  505. }
  506. };
  507. /**
  508. * Adjust the visible range such that the current time is located in the center
  509. * of the timeline
  510. */
  511. links.Timeline.prototype.setVisibleChartRangeNow = function() {
  512. var now = new Date();
  513. var diff = (this.end.getTime() - this.start.getTime());
  514. var startNew = new Date(now.getTime() - diff/2);
  515. var endNew = new Date(startNew.getTime() + diff);
  516. this.setVisibleChartRange(startNew, endNew);
  517. };
  518. /**
  519. * Retrieve the current visible range in the timeline.
  520. * @return {Object} An object with start and end properties
  521. */
  522. links.Timeline.prototype.getVisibleChartRange = function() {
  523. var range = {
  524. 'start': new Date(this.start),
  525. 'end': new Date(this.end)
  526. };
  527. return range;
  528. };
  529. /**
  530. * Redraw the timeline. This needs to be executed after the start and/or
  531. * end time are changed, or when data is added or removed dynamically.
  532. */
  533. links.Timeline.prototype.redrawFrame = function() {
  534. var dom = this.dom,
  535. options = this.options,
  536. size = this.size;
  537. if (!dom.frame) {
  538. // the surrounding main frame
  539. dom.frame = document.createElement("DIV");
  540. dom.frame.className = "timeline-frame";
  541. dom.frame.style.position = "relative";
  542. dom.frame.style.overflow = "hidden";
  543. dom.container.appendChild(dom.frame);
  544. }
  545. if (options.autoHeight) {
  546. dom.frame.style.height = size.frameHeight + "px";
  547. }
  548. else {
  549. dom.frame.style.height = options.height || "100%";
  550. }
  551. dom.frame.style.width = options.width || "100%";
  552. this.redrawContent();
  553. this.redrawGroups();
  554. this.redrawCurrentTime();
  555. this.redrawCustomTime();
  556. this.redrawNavigation();
  557. };
  558. /**
  559. * Redraw the content of the timeline: the axis and the items
  560. */
  561. links.Timeline.prototype.redrawContent = function() {
  562. var dom = this.dom,
  563. size = this.size;
  564. if (!dom.content) {
  565. // create content box where the axis and canvas will
  566. dom.content = document.createElement("DIV");
  567. //this.frame.className = "timeline-frame";
  568. dom.content.style.position = "relative";
  569. dom.content.style.overflow = "hidden";
  570. dom.frame.appendChild(dom.content);
  571. var timelines = document.createElement("DIV");
  572. timelines.style.position = "absolute";
  573. timelines.style.left = "0px";
  574. timelines.style.top = "0px";
  575. timelines.style.height = "100%";
  576. timelines.style.width = "0px";
  577. dom.content.appendChild(timelines);
  578. dom.contentTimelines = timelines;
  579. var params = this.eventParams,
  580. me = this;
  581. if (!params.onMouseDown) {
  582. params.onMouseDown = function (event) {me.onMouseDown(event);};
  583. links.Timeline.addEventListener(dom.content, "mousedown", params.onMouseDown);
  584. }
  585. if (!params.onTouchStart) {
  586. params.onTouchStart = function (event) {me.onTouchStart(event);};
  587. links.Timeline.addEventListener(dom.content, "touchstart", params.onTouchStart);
  588. }
  589. if (!params.onMouseWheel) {
  590. params.onMouseWheel = function (event) {me.onMouseWheel(event);};
  591. links.Timeline.addEventListener(dom.content, "mousewheel", params.onMouseWheel);
  592. }
  593. if (!params.onDblClick) {
  594. params.onDblClick = function (event) {me.onDblClick(event);};
  595. links.Timeline.addEventListener(dom.content, "dblclick", params.onDblClick);
  596. }
  597. }
  598. dom.content.style.left = size.contentLeft + "px";
  599. dom.content.style.top = "0px";
  600. dom.content.style.width = size.contentWidth + "px";
  601. dom.content.style.height = size.frameHeight + "px";
  602. this.redrawAxis();
  603. this.redrawItems();
  604. this.redrawDeleteButton();
  605. this.redrawDragAreas();
  606. };
  607. /**
  608. * Redraw the timeline axis with minor and major labels
  609. */
  610. links.Timeline.prototype.redrawAxis = function() {
  611. var dom = this.dom,
  612. options = this.options,
  613. size = this.size,
  614. step = this.step;
  615. var axis = dom.axis;
  616. if (!axis) {
  617. axis = {};
  618. dom.axis = axis;
  619. }
  620. if (size.axis.properties === undefined) {
  621. size.axis.properties = {};
  622. }
  623. if (axis.minorTexts === undefined) {
  624. axis.minorTexts = [];
  625. }
  626. if (axis.minorLines === undefined) {
  627. axis.minorLines = [];
  628. }
  629. if (axis.majorTexts === undefined) {
  630. axis.majorTexts = [];
  631. }
  632. if (axis.majorLines === undefined) {
  633. axis.majorLines = [];
  634. }
  635. if (!axis.frame) {
  636. axis.frame = document.createElement("DIV");
  637. axis.frame.style.position = "absolute";
  638. axis.frame.style.left = "0px";
  639. axis.frame.style.top = "0px";
  640. dom.content.appendChild(axis.frame);
  641. }
  642. // take axis offline
  643. dom.content.removeChild(axis.frame);
  644. axis.frame.style.width = (size.contentWidth) + "px";
  645. axis.frame.style.height = (size.axis.height) + "px";
  646. // the drawn axis is more wide than the actual visual part, such that
  647. // the axis can be dragged without having to redraw it each time again.
  648. var start = this.screenToTime(0);
  649. var end = this.screenToTime(size.contentWidth);
  650. var width = size.contentWidth;
  651. // calculate minimum step (in milliseconds) based on character size
  652. this.minimumStep = this.screenToTime(size.axis.characterMinorWidth * 6).valueOf() -
  653. this.screenToTime(0).valueOf();
  654. step.setRange(start, end, this.minimumStep);
  655. this.redrawAxisCharacters();
  656. this.redrawAxisStartOverwriting();
  657. step.start();
  658. var xFirstMajorLabel = undefined;
  659. while (!step.end()) {
  660. var cur = step.getCurrent(),
  661. x = this.timeToScreen(cur),
  662. isMajor = step.isMajor();
  663. this.redrawAxisMinorText(x, step.getLabelMinor());
  664. if (isMajor && options.showMajorLabels) {
  665. if (x > 0) {
  666. if (xFirstMajorLabel === undefined) {
  667. xFirstMajorLabel = x;
  668. }
  669. this.redrawAxisMajorText(x, step.getLabelMajor());
  670. }
  671. this.redrawAxisMajorLine(x);
  672. }
  673. else {
  674. this.redrawAxisMinorLine(x);
  675. }
  676. step.next();
  677. }
  678. // create a major label on the left when needed
  679. if (options.showMajorLabels) {
  680. var leftTime = this.screenToTime(0),
  681. leftText = this.step.getLabelMajor(leftTime),
  682. width = leftText.length * size.axis.characterMajorWidth + 10;// estimation
  683. if (xFirstMajorLabel === undefined || width < xFirstMajorLabel) {
  684. this.redrawAxisMajorText(0, leftText, leftTime);
  685. }
  686. }
  687. this.redrawAxisHorizontal();
  688. // cleanup left over labels
  689. this.redrawAxisEndOverwriting();
  690. // put axis online
  691. dom.content.insertBefore(axis.frame, dom.content.firstChild);
  692. };
  693. /**
  694. * Create characters used to determine the size of text on the axis
  695. */
  696. links.Timeline.prototype.redrawAxisCharacters = function () {
  697. // calculate the width and height of a single character
  698. // this is used to calculate the step size, and also the positioning of the
  699. // axis
  700. var dom = this.dom,
  701. axis = dom.axis;
  702. if (!axis.characterMinor) {
  703. var text = document.createTextNode("0");
  704. var characterMinor = document.createElement("DIV");
  705. characterMinor.className = "timeline-axis-text timeline-axis-text-minor";
  706. characterMinor.appendChild(text);
  707. characterMinor.style.position = "absolute";
  708. characterMinor.style.visibility = "hidden";
  709. characterMinor.style.paddingLeft = "0px";
  710. characterMinor.style.paddingRight = "0px";
  711. axis.frame.appendChild(characterMinor);
  712. axis.characterMinor = characterMinor;
  713. }
  714. if (!axis.characterMajor) {
  715. var text = document.createTextNode("0");
  716. var characterMajor = document.createElement("DIV");
  717. characterMajor.className = "timeline-axis-text timeline-axis-text-major";
  718. characterMajor.appendChild(text);
  719. characterMajor.style.position = "absolute";
  720. characterMajor.style.visibility = "hidden";
  721. characterMajor.style.paddingLeft = "0px";
  722. characterMajor.style.paddingRight = "0px";
  723. axis.frame.appendChild(characterMajor);
  724. axis.characterMajor = characterMajor;
  725. }
  726. };
  727. /**
  728. * Initialize redraw of the axis. All existing labels and lines will be
  729. * overwritten and reused.
  730. */
  731. links.Timeline.prototype.redrawAxisStartOverwriting = function () {
  732. var properties = this.size.axis.properties;
  733. properties.minorTextNum = 0;
  734. properties.minorLineNum = 0;
  735. properties.majorTextNum = 0;
  736. properties.majorLineNum = 0;
  737. };
  738. /**
  739. * End of overwriting HTML DOM elements of the axis.
  740. * remaining elements will be removed
  741. */
  742. links.Timeline.prototype.redrawAxisEndOverwriting = function () {
  743. var dom = this.dom,
  744. props = this.size.axis.properties,
  745. frame = this.dom.axis.frame;
  746. // remove leftovers
  747. var minorTexts = dom.axis.minorTexts,
  748. num = props.minorTextNum;
  749. while (minorTexts.length > num) {
  750. var minorText = minorTexts[num];
  751. frame.removeChild(minorText);
  752. minorTexts.splice(num, 1);
  753. }
  754. var minorLines = dom.axis.minorLines,
  755. num = props.minorLineNum;
  756. while (minorLines.length > num) {
  757. var minorLine = minorLines[num];
  758. frame.removeChild(minorLine);
  759. minorLines.splice(num, 1);
  760. }
  761. var majorTexts = dom.axis.majorTexts,
  762. num = props.majorTextNum;
  763. while (majorTexts.length > num) {
  764. var majorText = majorTexts[num];
  765. frame.removeChild(majorText);
  766. majorTexts.splice(num, 1);
  767. }
  768. var majorLines = dom.axis.majorLines,
  769. num = props.majorLineNum;
  770. while (majorLines.length > num) {
  771. var majorLine = majorLines[num];
  772. frame.removeChild(majorLine);
  773. majorLines.splice(num, 1);
  774. }
  775. };
  776. /**
  777. * Redraw the horizontal line and background of the axis
  778. */
  779. links.Timeline.prototype.redrawAxisHorizontal = function() {
  780. var axis = this.dom.axis,
  781. size = this.size;
  782. if (!axis.backgroundLine) {
  783. // create the axis line background (for a background color or so)
  784. var backgroundLine = document.createElement("DIV");
  785. backgroundLine.className = "timeline-axis";
  786. backgroundLine.style.position = "absolute";
  787. backgroundLine.style.left = "0px";
  788. backgroundLine.style.width = "100%";
  789. backgroundLine.style.border = "none";
  790. axis.frame.insertBefore(backgroundLine, axis.frame.firstChild);
  791. axis.backgroundLine = backgroundLine;
  792. }
  793. axis.backgroundLine.style.top = size.axis.top + "px";
  794. axis.backgroundLine.style.height = size.axis.height + "px";
  795. if (axis.line) {
  796. // put this line at the end of all childs
  797. var line = axis.frame.removeChild(axis.line);
  798. axis.frame.appendChild(line);
  799. }
  800. else {
  801. // make the axis line
  802. var line = document.createElement("DIV");
  803. line.className = "timeline-axis";
  804. line.style.position = "absolute";
  805. line.style.left = "0px";
  806. line.style.width = "100%";
  807. line.style.height = "0px";
  808. axis.frame.appendChild(line);
  809. axis.line = line;
  810. }
  811. axis.line.style.top = size.axis.line + "px";
  812. };
  813. /**
  814. * Create a minor label for the axis at position x
  815. * @param {Number} x
  816. * @param {String} text
  817. */
  818. links.Timeline.prototype.redrawAxisMinorText = function (x, text) {
  819. var size = this.size,
  820. dom = this.dom,
  821. props = size.axis.properties,
  822. frame = dom.axis.frame,
  823. minorTexts = dom.axis.minorTexts,
  824. index = props.minorTextNum,
  825. label;
  826. if (index < minorTexts.length) {
  827. label = minorTexts[index]
  828. }
  829. else {
  830. // create new label
  831. var content = document.createTextNode(""),
  832. label = document.createElement("DIV");
  833. label.appendChild(content);
  834. label.className = "timeline-axis-text timeline-axis-text-minor";
  835. label.style.position = "absolute";
  836. frame.appendChild(label);
  837. minorTexts.push(label);
  838. }
  839. label.childNodes[0].nodeValue = text;
  840. label.style.left = x + "px";
  841. label.style.top = size.axis.labelMinorTop + "px";
  842. //label.title = title; // TODO: this is a heavy operation
  843. props.minorTextNum++;
  844. };
  845. /**
  846. * Create a minor line for the axis at position x
  847. * @param {Number} x
  848. */
  849. links.Timeline.prototype.redrawAxisMinorLine = function (x) {
  850. var axis = this.size.axis,
  851. dom = this.dom,
  852. props = axis.properties,
  853. frame = dom.axis.frame,
  854. minorLines = dom.axis.minorLines,
  855. index = props.minorLineNum,
  856. line;
  857. if (index < minorLines.length) {
  858. line = minorLines[index];
  859. }
  860. else {
  861. // create vertical line
  862. line = document.createElement("DIV");
  863. line.className = "timeline-axis-grid timeline-axis-grid-minor";
  864. line.style.position = "absolute";
  865. line.style.width = "0px";
  866. frame.appendChild(line);
  867. minorLines.push(line);
  868. }
  869. line.style.top = axis.lineMinorTop + "px";
  870. line.style.height = axis.lineMinorHeight + "px";
  871. line.style.left = (x - axis.lineMinorWidth/2) + "px";
  872. props.minorLineNum++;
  873. };
  874. /**
  875. * Create a Major label for the axis at position x
  876. * @param {Number} x
  877. * @param {String} text
  878. */
  879. links.Timeline.prototype.redrawAxisMajorText = function (x, text) {
  880. var size = this.size,
  881. props = size.axis.properties,
  882. frame = this.dom.axis.frame,
  883. majorTexts = this.dom.axis.majorTexts,
  884. index = props.majorTextNum,
  885. label;
  886. if (index < majorTexts.length) {
  887. label = majorTexts[index];
  888. }
  889. else {
  890. // create label
  891. var content = document.createTextNode(text);
  892. label = document.createElement("DIV");
  893. label.className = "timeline-axis-text timeline-axis-text-major";
  894. label.appendChild(content);
  895. label.style.position = "absolute";
  896. label.style.top = "0px";
  897. frame.appendChild(label);
  898. majorTexts.push(label);
  899. }
  900. label.childNodes[0].nodeValue = text;
  901. label.style.top = size.axis.labelMajorTop + "px";
  902. label.style.left = x + "px";
  903. //label.title = title; // TODO: this is a heavy operation
  904. props.majorTextNum ++;
  905. };
  906. /**
  907. * Create a Major line for the axis at position x
  908. * @param {Number} x
  909. */
  910. links.Timeline.prototype.redrawAxisMajorLine = function (x) {
  911. var size = this.size,
  912. props = size.axis.properties,
  913. axis = this.size.axis,
  914. frame = this.dom.axis.frame,
  915. majorLines = this.dom.axis.majorLines,
  916. index = props.majorLineNum,
  917. line;
  918. if (index < majorLines.length) {
  919. var line = majorLines[index];
  920. }
  921. else {
  922. // create vertical line
  923. line = document.createElement("DIV");
  924. line.className = "timeline-axis-grid timeline-axis-grid-major";
  925. line.style.position = "absolute";
  926. line.style.top = "0px";
  927. line.style.width = "0px";
  928. frame.appendChild(line);
  929. majorLines.push(line);
  930. }
  931. line.style.left = (x - axis.lineMajorWidth/2) + "px";
  932. line.style.height = size.frameHeight + "px";
  933. props.majorLineNum ++;
  934. };
  935. /**
  936. * Redraw all items
  937. */
  938. links.Timeline.prototype.redrawItems = function() {
  939. var dom = this.dom,
  940. options = this.options,
  941. boxAlign = (options.box && options.box.align) ? options.box.align : undefined,
  942. size = this.size,
  943. contentWidth = size.contentWidth,
  944. items = this.items;
  945. if (!dom.items) {
  946. dom.items = {};
  947. }
  948. // draw the frame containing the items
  949. var frame = dom.items.frame;
  950. if (!frame) {
  951. frame = document.createElement("DIV");
  952. frame.style.position = "relative";
  953. dom.content.appendChild(frame);
  954. dom.items.frame = frame;
  955. }
  956. frame.style.left = "0px";
  957. //frame.style.width = "0px";
  958. frame.style.top = size.items.top + "px";
  959. frame.style.height = (size.frameHeight - size.axis.height) + "px";
  960. // initialize arrarys for storing the items
  961. var ranges = dom.items.ranges;
  962. if (!ranges) {
  963. ranges = [];
  964. dom.items.ranges = ranges;
  965. }
  966. var boxes = dom.items.boxes;
  967. if (!boxes) {
  968. boxes = [];
  969. dom.items.boxes = boxes;
  970. }
  971. var dots = dom.items.dots;
  972. if (!dots) {
  973. dots = [];
  974. dom.items.dots = dots;
  975. }
  976. // Take frame offline
  977. dom.content.removeChild(frame);
  978. if (size.dataChanged) {
  979. // create the items
  980. var rangesCreated = ranges.length,
  981. boxesCreated = boxes.length,
  982. dotsCreated = dots.length,
  983. rangesUsed = 0,
  984. boxesUsed = 0,
  985. dotsUsed = 0,
  986. itemsLength = items.length;
  987. for (var i = 0, iMax = items.length; i < iMax; i++) {
  988. var item = items[i];
  989. switch (item.type) {
  990. case 'range':
  991. if (rangesUsed < rangesCreated) {
  992. // reuse existing range
  993. var domItem = ranges[rangesUsed];
  994. domItem.firstChild.innerHTML = item.content;
  995. domItem.style.display = '';
  996. item.dom = domItem;
  997. rangesUsed++;
  998. }
  999. else {
  1000. // create a new range
  1001. var domItem = this.createEventRange(item.content);
  1002. ranges[rangesUsed] = domItem;
  1003. frame.appendChild(domItem);
  1004. item.dom = domItem;
  1005. rangesUsed++;
  1006. rangesCreated++;
  1007. }
  1008. break;
  1009. case 'box':
  1010. if (boxesUsed < boxesCreated) {
  1011. // reuse existing box
  1012. var domItem = boxes[boxesUsed];
  1013. domItem.firstChild.innerHTML = item.content;
  1014. domItem.style.display = '';
  1015. item.dom = domItem;
  1016. boxesUsed++;
  1017. }
  1018. else {
  1019. // create a new box
  1020. var domItem = this.createEventBox(item.content);
  1021. boxes[boxesUsed] = domItem;
  1022. frame.appendChild(domItem);
  1023. frame.insertBefore(domItem.line, frame.firstChild);
  1024. // Note: line must be added in front of the items,
  1025. // such that it stays below all items
  1026. frame.appendChild(domItem.dot);
  1027. item.dom = domItem;
  1028. boxesUsed++;
  1029. boxesCreated++;
  1030. }
  1031. break;
  1032. case 'dot':
  1033. if (dotsUsed < dotsCreated) {
  1034. // reuse existing box
  1035. var domItem = dots[dotsUsed];
  1036. domItem.firstChild.innerHTML = item.content;
  1037. domItem.style.display = '';
  1038. item.dom = domItem;
  1039. dotsUsed++;
  1040. }
  1041. else {
  1042. // create a new box
  1043. var domItem = this.createEventDot(item.content);
  1044. dots[dotsUsed] = domItem;
  1045. frame.appendChild(domItem);
  1046. item.dom = domItem;
  1047. dotsUsed++;
  1048. dotsCreated++;
  1049. }
  1050. break;
  1051. default:
  1052. // do nothing
  1053. break;
  1054. }
  1055. }
  1056. // remove redundant items when needed
  1057. for (var i = rangesUsed; i < rangesCreated; i++) {
  1058. frame.removeChild(ranges[i]);
  1059. }
  1060. ranges.splice(rangesUsed, rangesCreated - rangesUsed);
  1061. for (var i = boxesUsed; i < boxesCreated; i++) {
  1062. var box = boxes[i];
  1063. frame.removeChild(box.line);
  1064. frame.removeChild(box.dot);
  1065. frame.removeChild(box);
  1066. }
  1067. boxes.splice(boxesUsed, boxesCreated - boxesUsed);
  1068. for (var i = dotsUsed; i < dotsCreated; i++) {
  1069. frame.removeChild(dots[i]);
  1070. }
  1071. dots.splice(dotsUsed, dotsCreated - dotsUsed);
  1072. }
  1073. // reposition all items
  1074. for (var i = 0, iMax = items.length; i < iMax; i++) {
  1075. var item = items[i],
  1076. domItem = item.dom;
  1077. switch (item.type) {
  1078. case 'range':
  1079. var left = this.timeToScreen(item.start),
  1080. right = this.timeToScreen(item.end);
  1081. // limit the width of the item, as browsers cannot draw very wide divs
  1082. if (left < -contentWidth) {
  1083. left = -contentWidth;
  1084. }
  1085. if (right > 2 * contentWidth) {
  1086. right = 2 * contentWidth;
  1087. }
  1088. var visible = right > -contentWidth && left < 2 * contentWidth;
  1089. if (visible || size.dataChanged) {
  1090. // when data is changed, all items must be kept visible, as their heights must be measured
  1091. if (item.hidden) {
  1092. item.hidden = false;
  1093. domItem.style.display = '';
  1094. }
  1095. domItem.style.top = item.top + "px";
  1096. domItem.style.left = left + "px";
  1097. //domItem.style.width = Math.max(right - left - 2 * item.borderWidth, 1) + "px"; // TODO: borderWidth
  1098. domItem.style.width = Math.max(right - left, 1) + "px";
  1099. }
  1100. else {
  1101. // hide when outside of the current window
  1102. if (!item.hidden) {
  1103. domItem.style.display = 'none';
  1104. item.hidden = true;
  1105. }
  1106. }
  1107. break;
  1108. case 'box':
  1109. var left = this.timeToScreen(item.start);
  1110. var axisOnTop = options.axisOnTop,
  1111. axisHeight = size.axis.height,
  1112. axisTop = size.axis.top;
  1113. var visible = ((left + item.width/2 > -contentWidth) &&
  1114. (left - item.width/2 < 2 * contentWidth));
  1115. if (visible || size.dataChanged) {
  1116. // when data is changed, all items must be kept visible, as their heights must be measured
  1117. if (item.hidden) {
  1118. item.hidden = false;
  1119. domItem.style.display = '';
  1120. domItem.line.style.display = '';
  1121. domItem.dot.style.display = '';
  1122. }
  1123. domItem.style.top = item.top + "px";
  1124. if (boxAlign == 'right') {
  1125. domItem.style.left = (left - item.width) + "px";
  1126. }
  1127. else if (boxAlign == 'left') {
  1128. domItem.style.left = (left) + "px";
  1129. }
  1130. else { // default or 'center'
  1131. domItem.style.left = (left - item.width/2) + "px";
  1132. }
  1133. var line = domItem.line;
  1134. line.style.left = (left - item.lineWidth/2) + "px";
  1135. if (axisOnTop) {
  1136. line.style.top = "0px";
  1137. line.style.height = Math.max(item.top, 0) + "px";
  1138. }
  1139. else {
  1140. line.style.top = (item.top + item.height) + "px";
  1141. line.style.height = Math.max(axisTop - item.top - item.height, 0) + "px";
  1142. }
  1143. var dot = domItem.dot;
  1144. dot.style.left = (left - item.dotWidth/2) + "px";
  1145. dot.style.top = (axisTop - item.dotHeight/2) + "px";
  1146. }
  1147. else {
  1148. // hide when outside of the current window
  1149. if (!item.hidden) {
  1150. domItem.style.display = 'none';
  1151. domItem.line.style.display = 'none';
  1152. domItem.dot.style.display = 'none';
  1153. item.hidden = true;
  1154. }
  1155. }
  1156. break;
  1157. case 'dot':
  1158. var left = this.timeToScreen(item.start);
  1159. var axisOnTop = options.axisOnTop,
  1160. axisHeight = size.axis.height,
  1161. axisTop = size.axis.top;
  1162. var visible = (left + item.width > -contentWidth) && (left < 2 * contentWidth);
  1163. if (visible || size.dataChanged) {
  1164. // when data is changed, all items must be kept visible, as their heights must be measured
  1165. if (item.hidden) {
  1166. item.hidden = false;
  1167. domItem.style.display = '';
  1168. }
  1169. domItem.style.top = item.top + "px";
  1170. domItem.style.left = (left - item.dotWidth / 2) + "px";
  1171. domItem.content.style.marginLeft = (1.5 * item.dotWidth) + "px";
  1172. //domItem.content.style.marginRight = (0.5 * item.dotWidth) + "px"; // TODO
  1173. domItem.dot.style.top = ((item.height - item.dotHeight) / 2) + "px";
  1174. }
  1175. else {
  1176. // hide when outside of the current window
  1177. if (!item.hidden) {
  1178. domItem.style.display = 'none';
  1179. item.hidden = true;
  1180. }
  1181. }
  1182. break;
  1183. default:
  1184. // do nothing
  1185. break;
  1186. }
  1187. }
  1188. // move selected item to the end, to ensure that it is always on top
  1189. if (this.selection) {
  1190. var item = this.selection.item;
  1191. frame.removeChild(item);
  1192. frame.appendChild(item);
  1193. }
  1194. // put frame online again
  1195. dom.content.appendChild(frame);
  1196. /* TODO
  1197. // retrieve all image sources from the items, and set a callback once
  1198. // all images are retrieved
  1199. var urls = [];
  1200. var timeline = this;
  1201. links.Timeline.filterImageUrls(frame, urls);
  1202. if (urls.length) {
  1203. for (var i = 0; i < urls.length; i++) {
  1204. var url = urls[i];
  1205. var callback = function (url) {
  1206. timeline.redraw();
  1207. };
  1208. var sendCallbackWhenAlreadyLoaded = false;
  1209. links.imageloader.load(url, callback, sendCallbackWhenAlreadyLoaded);
  1210. }
  1211. }
  1212. */
  1213. };
  1214. /**
  1215. * Create an event in the timeline, with (optional) formatting: inside a box
  1216. * with rounded corners, and a vertical line+dot to the axis.
  1217. * @param {string} content The content for the event. This can be plain text
  1218. * or HTML code.
  1219. */
  1220. links.Timeline.prototype.createEventBox = function(content) {
  1221. // background box
  1222. var divBox = document.createElement("DIV");
  1223. divBox.style.position = "absolute";
  1224. divBox.style.left = "0px";
  1225. divBox.style.top = "0px";
  1226. divBox.className = "timeline-event timeline-event-box";
  1227. // contents box (inside the background box). used for making margins
  1228. var divContent = document.createElement("DIV");
  1229. divContent.className = "timeline-event-content";
  1230. divContent.innerHTML = content;
  1231. divBox.appendChild(divContent);
  1232. // line to axis
  1233. var divLine = document.createElement("DIV");
  1234. divLine.style.position = "absolute";
  1235. divLine.style.width = "0px";
  1236. divLine.className = "timeline-event timeline-event-line";
  1237. // important: the vertical line is added at the front of the list of elements,
  1238. // so it will be drawn behind all boxes and ranges
  1239. divBox.line = divLine;
  1240. // dot on axis
  1241. var divDot = document.createElement("DIV");
  1242. divDot.style.position = "absolute";
  1243. divDot.style.width = "0px";
  1244. divDot.style.height = "0px";
  1245. divDot.className = "timeline-event timeline-event-dot";
  1246. divBox.dot = divDot;
  1247. return divBox;
  1248. };
  1249. /**
  1250. * Create an event in the timeline: a dot, followed by the content.
  1251. * @param {string} content The content for the event. This can be plain text
  1252. * or HTML code.
  1253. */
  1254. links.Timeline.prototype.createEventDot = function(content) {
  1255. // background box
  1256. var divBox = document.createElement("DIV");
  1257. divBox.style.position = "absolute";
  1258. // contents box, right from the dot
  1259. var divContent = document.createElement("DIV");
  1260. divContent.className = "timeline-event-content";
  1261. divContent.innerHTML = content;
  1262. divBox.appendChild(divContent);
  1263. // dot at start
  1264. var divDot = document.createElement("DIV");
  1265. divDot.style.position = "absolute";
  1266. divDot.className = "timeline-event timeline-event-dot";
  1267. divDot.style.width = "0px";
  1268. divDot.style.height = "0px";
  1269. divBox.appendChild(divDot);
  1270. divBox.content = divContent;
  1271. divBox.dot = divDot;
  1272. return divBox;
  1273. };
  1274. /**
  1275. * Create an event range as a beam in the timeline.
  1276. * @param {string} content The content for the event. This can be plain text
  1277. * or HTML code.
  1278. */
  1279. links.Timeline.prototype.createEventRange = function(content) {
  1280. // background box
  1281. var divBox = document.createElement("DIV");
  1282. divBox.style.position = "absolute";
  1283. divBox.className = "timeline-event timeline-event-range";
  1284. // contents box
  1285. var divContent = document.createElement("DIV");
  1286. divContent.className = "timeline-event-content";
  1287. divContent.innerHTML = content;
  1288. divBox.appendChild(divContent);
  1289. return divBox;
  1290. };
  1291. /**
  1292. * Redraw the group labels
  1293. */
  1294. links.Timeline.prototype.redrawGroups = function() {
  1295. var dom = this.dom,
  1296. options = this.options,
  1297. size = this.size,
  1298. groups = this.groups;
  1299. if (dom.groups === undefined) {
  1300. dom.groups = {};
  1301. }
  1302. var labels = dom.groups.labels;
  1303. if (!labels) {
  1304. labels = [];
  1305. dom.groups.labels = labels;
  1306. }
  1307. var labelLines = dom.groups.labelLines;
  1308. if (!labelLines) {
  1309. labelLines = [];
  1310. dom.groups.labelLines = labelLines;
  1311. }
  1312. var itemLines = dom.groups.itemLines;
  1313. if (!itemLines) {
  1314. itemLines = [];
  1315. dom.groups.itemLines = itemLines;
  1316. }
  1317. // create the frame for holding the groups
  1318. var frame = dom.groups.frame;
  1319. if (!frame) {
  1320. frame = document.createElement("DIV");
  1321. frame.className = "timeline-groups-axis";
  1322. frame.style.position = "absolute";
  1323. frame.style.overflow = "hidden";
  1324. frame.style.top = "0px";
  1325. frame.style.height = "100%";
  1326. dom.frame.appendChild(frame);
  1327. dom.groups.frame = frame;
  1328. }
  1329. frame.style.left = size.groupsLeft + "px";
  1330. frame.style.width = (options.groupsWidth !== undefined) ?
  1331. options.groupsWidth :
  1332. size.groupsWidth + "px";
  1333. // hide groups axis when there are no groups
  1334. if (groups.length == 0) {
  1335. frame.style.display = 'none';
  1336. }
  1337. else {
  1338. frame.style.display = '';
  1339. }
  1340. if (size.dataChanged) {
  1341. // create the items
  1342. var current = labels.length,
  1343. needed = groups.length;
  1344. // overwrite existing items
  1345. for (var i = 0, iMax = Math.min(current, needed); i < iMax; i++) {
  1346. var group = groups[i];
  1347. var label = labels[i];
  1348. label.innerHTML = group.content;
  1349. label.style.display = '';
  1350. }
  1351. // append new items when needed
  1352. for (var i = current; i < needed; i++) {
  1353. var group = groups[i];
  1354. // create text label
  1355. var label = document.createElement("DIV");
  1356. label.className = "timeline-groups-text";
  1357. label.style.position = "absolute";
  1358. if (options.groupsWidth === undefined) {
  1359. label.style.whiteSpace = "nowrap";
  1360. }
  1361. label.innerHTML = group.content;
  1362. frame.appendChild(label);
  1363. labels[i] = label;
  1364. // create the grid line between the group labels
  1365. var labelLine = document.createElement("DIV");
  1366. labelLine.className = "timeline-axis-grid timeline-axis-grid-minor";
  1367. labelLine.style.position = "absolute";
  1368. labelLine.style.left = "0px";
  1369. labelLine.style.width = "100%";
  1370. labelLine.style.height = "0px";
  1371. labelLine.style.borderTopStyle = "solid";
  1372. frame.appendChild(labelLine);
  1373. labelLines[i] = labelLine;
  1374. // create the grid line between the items
  1375. var itemLine = document.createElement("DIV");
  1376. itemLine.className = "timeline-axis-grid timeline-axis-grid-minor";
  1377. itemLine.style.position = "absolute";
  1378. itemLine.style.left = "0px";
  1379. itemLine.style.width = "100%";
  1380. itemLine.style.height = "0px";
  1381. itemLine.style.borderTopStyle = "solid";
  1382. dom.content.insertBefore(itemLine, dom.content.firstChild);
  1383. itemLines[i] = itemLine;
  1384. }
  1385. // remove redundant items from the DOM when needed
  1386. for (var i = needed; i < current; i++) {
  1387. var label = labels[i],
  1388. labelLine = labelLines[i],
  1389. itemLine = itemLines[i];
  1390. frame.removeChild(label);
  1391. frame.removeChild(labelLine);
  1392. dom.content.removeChild(itemLine);
  1393. }
  1394. labels.splice(needed, current - needed);
  1395. labelLines.splice(needed, current - needed);
  1396. itemLines.splice(needed, current - needed);
  1397. frame.style.borderStyle = options.groupsOnRight ?
  1398. "none none none solid" :
  1399. "none solid none none";
  1400. }
  1401. // position the groups
  1402. for (var i = 0, iMax = groups.length; i < iMax; i++) {
  1403. var group = groups[i],
  1404. label = labels[i],
  1405. labelLine = labelLines[i],
  1406. itemLine = itemLines[i];
  1407. label.style.top = group.labelTop + "px";
  1408. labelLine.style.top = group.lineTop + "px";
  1409. itemLine.style.top = group.lineTop + "px";
  1410. itemLine.style.width = size.contentWidth + "px";
  1411. }
  1412. if (!dom.groups.background) {
  1413. // create the axis grid line background
  1414. var background = document.createElement("DIV");
  1415. background.className = "timeline-axis";
  1416. background.style.position = "absolute";
  1417. background.style.left = "0px";
  1418. background.style.width = "100%";
  1419. background.style.border = "none";
  1420. frame.appendChild(background);
  1421. dom.groups.background = background;
  1422. }
  1423. dom.groups.background.style.top = size.axis.top + 'px';
  1424. dom.groups.background.style.height = size.axis.height + 'px';
  1425. if (!dom.groups.line) {
  1426. // create the axis grid line
  1427. var line = document.createElement("DIV");
  1428. line.className = "timeline-axis";
  1429. line.style.position = "absolute";
  1430. line.style.left = "0px";
  1431. line.style.width = "100%";
  1432. line.style.height = "0px";
  1433. frame.appendChild(line);
  1434. dom.groups.line = line;
  1435. }
  1436. dom.groups.line.style.top = size.axis.line + 'px';
  1437. };
  1438. /**
  1439. * Redraw the current time bar
  1440. */
  1441. links.Timeline.prototype.redrawCurrentTime = function() {
  1442. var options = this.options,
  1443. dom = this.dom,
  1444. size = this.size;
  1445. if (!options.showCurrentTime) {
  1446. if (dom.currentTime) {
  1447. dom.contentTimelines.removeChild(dom.currentTime);
  1448. delete dom.currentTime;
  1449. }
  1450. return;
  1451. }
  1452. if (!dom.currentTime) {
  1453. // create the current time bar
  1454. var currentTime = document.createElement("DIV");
  1455. currentTime.className = "timeline-currenttime";
  1456. currentTime.style.position = "absolute";
  1457. currentTime.style.top = "0px";
  1458. currentTime.style.height = "100%";
  1459. dom.contentTimelines.appendChild(currentTime);
  1460. dom.currentTime = currentTime;
  1461. }
  1462. var now = new Date();
  1463. var nowOffset = new Date(now.getTime() + this.clientTimeOffset);
  1464. var x = this.timeToScreen(nowOffset);
  1465. var visible = (x > -size.contentWidth && x < 2 * size.contentWidth);
  1466. dom.currentTime.style.display = visible ? '' : 'none';
  1467. dom.currentTime.style.left = x + "px";
  1468. dom.currentTime.title = "Current time: " + nowOffset;
  1469. // start a timer to adjust for the new time
  1470. if (this.currentTimeTimer != undefined) {
  1471. clearTimeout(this.currentTimeTimer);
  1472. delete this.currentTimeTimer;
  1473. }
  1474. var timeline = this;
  1475. var onTimeout = function() {
  1476. timeline.redrawCurrentTime();
  1477. };
  1478. // the time equal to the width of one pixel, divided by 2 for more smoothness
  1479. var interval = 1 / this.conversion.factor / 2;
  1480. if (interval < 30) interval = 30;
  1481. this.currentTimeTimer = setTimeout(onTimeout, interval);
  1482. };
  1483. /**
  1484. * Redraw the custom time bar
  1485. */
  1486. links.Timeline.prototype.redrawCustomTime = function() {
  1487. var options = this.options,
  1488. dom = this.dom,
  1489. size = this.size;
  1490. if (!options.showCustomTime) {
  1491. if (dom.customTime) {
  1492. dom.contentTimelines.removeChild(dom.customTime);
  1493. delete dom.customTime;
  1494. }
  1495. return;
  1496. }
  1497. if (!dom.customTime) {
  1498. var customTime = document.createElement("DIV");
  1499. customTime.className = "timeline-customtime";
  1500. customTime.style.position = "absolute";
  1501. customTime.style.top = "0px";
  1502. customTime.style.height = "100%";
  1503. var drag = document.createElement("DIV");
  1504. drag.style.position = "relative";
  1505. drag.style.top = "0px";
  1506. drag.style.left = "-10px";
  1507. drag.style.height = "100%";
  1508. drag.style.width = "20px";
  1509. customTime.appendChild(drag);
  1510. dom.contentTimelines.appendChild(customTime);
  1511. dom.customTime = customTime;
  1512. // initialize parameter
  1513. this.customTime = new Date();
  1514. }
  1515. var x = this.timeToScreen(this.customTime),
  1516. visible = (x > -size.contentWidth && x < 2 * size.contentWidth);
  1517. dom.customTime.style.display = visible ? '' : 'none';
  1518. dom.customTime.style.left = x + "px";
  1519. dom.customTime.title = "Time: " + this.customTime;
  1520. };
  1521. /**
  1522. * Redraw the delete button, on the top right of the currently selected item
  1523. * if there is no item selected, the button is hidden.
  1524. */
  1525. links.Timeline.prototype.redrawDeleteButton = function () {
  1526. var timeline = this,
  1527. options = this.options,
  1528. dom = this.dom,
  1529. size = this.size,
  1530. frame = dom.items.frame;
  1531. if (!options.editable) {
  1532. return;
  1533. }
  1534. var deleteButton = dom.items.deleteButton;
  1535. if (!deleteButton) {
  1536. // create a delete button
  1537. deleteButton = document.createElement("DIV");
  1538. deleteButton.className = "timeline-navigation-delete";
  1539. deleteButton.style.position = "absolute";
  1540. frame.appendChild(deleteButton);
  1541. dom.items.deleteButton = deleteButton;
  1542. }
  1543. if (this.selection) {
  1544. var index = this.selection.index,
  1545. item = this.items[index],
  1546. domItem = this.selection.item,
  1547. right,
  1548. top = item.top;
  1549. switch (item.type) {
  1550. case 'range':
  1551. right = this.timeToScreen(item.end);
  1552. break;
  1553. case 'box':
  1554. //right = this.timeToScreen(item.start) + item.width / 2 + item.borderWidth; // TODO: borderWidth
  1555. right = this.timeToScreen(item.start) + item.width / 2;
  1556. break;
  1557. case 'dot':
  1558. right = this.timeToScreen(item.start) + item.width;
  1559. break;
  1560. }
  1561. // limit the position
  1562. if (right < -size.contentWidth) {
  1563. right = -size.contentWidth;
  1564. }
  1565. if (right > 2 * size.contentWidth) {
  1566. right = 2 * size.contentWidth;
  1567. }
  1568. deleteButton.style.left = right + 'px';
  1569. deleteButton.style.top = top + 'px';
  1570. deleteButton.style.display = '';
  1571. frame.removeChild(deleteButton);
  1572. frame.appendChild(deleteButton);
  1573. }
  1574. else {
  1575. deleteButton.style.display = 'none';
  1576. }
  1577. };
  1578. /**
  1579. * Redraw the drag areas. When an item (ranges only) is selected,
  1580. * it gets a drag area on the left and right side, to change its width
  1581. */
  1582. links.Timeline.prototype.redrawDragAreas = function () {
  1583. var timeline = this,
  1584. options = this.options,
  1585. dom = this.dom,
  1586. size = this.size,
  1587. frame = this.dom.items.frame;
  1588. if (!options.editable) {
  1589. return;
  1590. }
  1591. // create left drag area
  1592. var dragLeft = dom.items.dragLeft;
  1593. if (!dragLeft) {
  1594. dragLeft = document.createElement("DIV");
  1595. dragLeft.className="timeline-event-range-drag-left";
  1596. dragLeft.style.width = options.dragAreaWidth + "px";
  1597. dragLeft.style.position = "absolute";
  1598. frame.appendChild(dragLeft);
  1599. dom.items.dragLeft = dragLeft;
  1600. }
  1601. // create right drag area
  1602. var dragRight = dom.items.dragRight;
  1603. if (!dragRight) {
  1604. dragRight = document.createElement("DIV");
  1605. dragRight.className="timeline-event-range-drag-right";
  1606. dragRight.style.width = options.dragAreaWidth + "px";
  1607. dragRight.style.position = "absolute";
  1608. frame.appendChild(dragRight);
  1609. dom.items.dragRight = dragRight;
  1610. }
  1611. // reposition left and right drag area
  1612. if (this.selection) {
  1613. var index = this.selection.index,
  1614. item = this.items[index];
  1615. if (item.type == 'range') {
  1616. var domItem = item.dom,
  1617. left = this.timeToScreen(item.start),
  1618. right = this.timeToScreen(item.end),
  1619. top = item.top,
  1620. height = item.height;
  1621. dragLeft.style.left = left + 'px';
  1622. dragLeft.style.top = top + 'px';
  1623. dragLeft.style.height = height + 'px';
  1624. dragLeft.style.display = '';
  1625. frame.removeChild(dragLeft);
  1626. frame.appendChild(dragLeft);
  1627. dragRight.style.left = (right - options.dragAreaWidth) + 'px';
  1628. dragRight.style.top = top + 'px';
  1629. dragRight.style.height = height + 'px';
  1630. dragRight.style.display = '';
  1631. frame.removeChild(dragRight);
  1632. frame.appendChild(dragRight);
  1633. }
  1634. }
  1635. else {
  1636. dragLeft.style.display = 'none';
  1637. dragRight.style.display = 'none';
  1638. }
  1639. };
  1640. /**
  1641. * Create the navigation buttons for zooming and moving
  1642. */
  1643. links.Timeline.prototype.redrawNavigation = function () {
  1644. var timeline = this,
  1645. options = this.options,
  1646. dom = this.dom,
  1647. frame = dom.frame,
  1648. navBar = dom.navBar;
  1649. if (!navBar) {
  1650. if (options.editable || options.showNavigation) {
  1651. // create a navigation bar containing the navigation buttons
  1652. navBar = document.createElement("DIV");
  1653. navBar.style.position = "absolute";
  1654. navBar.className = "timeline-navigation";
  1655. if (options.groupsOnRight) {
  1656. navBar.style.left = '10px';
  1657. }
  1658. else {
  1659. navBar.style.right = '10px';
  1660. }
  1661. if (options.axisOnTop) {
  1662. navBar.style.bottom = '10px';
  1663. }
  1664. else {
  1665. navBar.style.top = '10px';
  1666. }
  1667. dom.navBar = navBar;
  1668. frame.appendChild(navBar);
  1669. }
  1670. if (options.editable && options.showButtonAdd) {
  1671. // create a new in button
  1672. navBar.addButton = document.createElement("DIV");
  1673. navBar.addButton.className = "timeline-navigation-new";
  1674. navBar.addButton.title = "Create new event";
  1675. var onAdd = function(event) {
  1676. links.Timeline.preventDefault(event);
  1677. links.Timeline.stopPropagation(event);
  1678. // create a new event at the center of the frame
  1679. var w = timeline.size.contentWidth;
  1680. var x = w / 2;
  1681. var xstart = timeline.screenToTime(x - w / 10); // subtract 10% of timeline width
  1682. var xend = timeline.screenToTime(x + w / 10); // add 10% of timeline width
  1683. if (options.snapEvents) {
  1684. timeline.step.snap(xstart);
  1685. timeline.step.snap(xend);
  1686. }
  1687. var content = "New";
  1688. var group = timeline.groups.length ? timeline.groups[0].content : undefined;
  1689. timeline.addItem({
  1690. 'start': xstart,
  1691. 'end': xend,
  1692. 'content': content,
  1693. 'group': group
  1694. });
  1695. var index = (timeline.items.length - 1);
  1696. timeline.selectItem(index);
  1697. timeline.applyAdd = true;
  1698. // fire an add event.
  1699. // Note that the change can be canceled from within an event listener if
  1700. // this listener calls the method cancelAdd().
  1701. timeline.trigger('add');
  1702. if (!timeline.applyAdd) {
  1703. // undo an add
  1704. timeline.deleteItem(index);
  1705. }
  1706. timeline.redrawDeleteButton();
  1707. timeline.redrawDragAreas();
  1708. };
  1709. links.Timeline.addEventListener(navBar.addButton, "mousedown", onAdd);
  1710. navBar.appendChild(navBar.addButton);
  1711. }
  1712. if (options.editable && options.showButtonAdd && options.showNavigation) {
  1713. // create a separator line
  1714. navBar.addButton.style.borderRightWidth = "1px";
  1715. navBar.addButton.style.borderRightStyle = "solid";
  1716. }
  1717. if (options.showNavigation) {
  1718. // create a zoom in button
  1719. navBar.zoomInButton = document.createElement("DIV");
  1720. navBar.zoomInButton.className = "timeline-navigation-zoom-in";
  1721. navBar.zoomInButton.title = "Zoom in";
  1722. var onZoomIn = function(event) {
  1723. links.Timeline.preventDefault(event);
  1724. links.Timeline.stopPropagation(event);
  1725. timeline.zoom(0.4);
  1726. timeline.trigger("rangechange");
  1727. timeline.trigger("rangechanged");
  1728. };
  1729. links.Timeline.addEventListener(navBar.zoomInButton, "mousedown", onZoomIn);
  1730. navBar.appendChild(navBar.zoomInButton);
  1731. // create a zoom out button
  1732. navBar.zoomOutButton = document.createElement("DIV");
  1733. navBar.zoomOutButton.className = "timeline-navigation-zoom-out";
  1734. navBar.zoomOutButton.title = "Zoom out";
  1735. var onZoomOut = function(event) {
  1736. links.Timeline.preventDefault(event);
  1737. links.Timeline.stopPropagation(event);
  1738. timeline.zoom(-0.4);
  1739. timeline.trigger("rangechange");
  1740. timeline.trigger("rangechanged");
  1741. };
  1742. links.Timeline.addEventListener(navBar.zoomOutButton, "mousedown", onZoomOut);
  1743. navBar.appendChild(navBar.zoomOutButton);
  1744. // create a move left button
  1745. navBar.moveLeftButton = document.createElement("DIV");
  1746. navBar.moveLeftButton.className = "timeline-navigation-move-left";
  1747. navBar.moveLeftButton.title = "Move left";
  1748. var onMoveLeft = function(event) {
  1749. links.Timeline.preventDefault(event);
  1750. links.Timeline.stopPropagation(event);
  1751. timeline.move(-0.2);
  1752. timeline.trigger("rangechange");
  1753. timeline.trigger("rangechanged");
  1754. };
  1755. links.Timeline.addEventListener(navBar.moveLeftButton, "mousedown", onMoveLeft);
  1756. navBar.appendChild(navBar.moveLeftButton);
  1757. // create a move right button
  1758. navBar.moveRightButton = document.createElement("DIV");
  1759. navBar.moveRightButton.className = "timeline-navigation-move-right";
  1760. navBar.moveRightButton.title = "Move right";
  1761. var onMoveRight = function(event) {
  1762. links.Timeline.preventDefault(event);
  1763. links.Timeline.stopPropagation(event);
  1764. timeline.move(0.2);
  1765. timeline.trigger("rangechange");
  1766. timeline.trigger("rangechanged");
  1767. };
  1768. links.Timeline.addEventListener(navBar.moveRightButton, "mousedown", onMoveRight);
  1769. navBar.appendChild(navBar.moveRightButton);
  1770. }
  1771. }
  1772. };
  1773. /**
  1774. * Set current time. This function can be used to set the time in the client
  1775. * timeline equal with the time on a server.
  1776. * @param {Date} time
  1777. */
  1778. links.Timeline.prototype.setCurrentTime = function(time) {
  1779. var now = new Date();
  1780. this.clientTimeOffset = time.getTime() - now.getTime();
  1781. this.redrawCurrentTime();
  1782. };
  1783. /**
  1784. * Get current time. The time can have an offset from the real time, when
  1785. * the current time has been changed via the method setCurrentTime.
  1786. * @return {Date} time
  1787. */
  1788. links.Timeline.prototype.getCurrentTime = function() {
  1789. var now = new Date();
  1790. return new Date(now.getTime() + this.clientTimeOffset);
  1791. };
  1792. /**
  1793. * Set custom time.
  1794. * The custom time bar can be used to display events in past or future.
  1795. * @param {Date} time
  1796. */
  1797. links.Timeline.prototype.setCustomTime = function(time) {
  1798. this.customTime = new Date(time);
  1799. this.redrawCustomTime();
  1800. };
  1801. /**
  1802. * Retrieve the current custom time.
  1803. * @return {Date} customTime
  1804. */
  1805. links.Timeline.prototype.getCustomTime = function() {
  1806. return new Date(this.customTime);
  1807. };
  1808. /**
  1809. * Set a custom scale. Autoscaling will be disabled.
  1810. * For example setScale(SCALE.MINUTES, 5) will result
  1811. * in minor steps of 5 minutes, and major steps of an hour.
  1812. *
  1813. * @param {links.Timeline.StepDate.SCALE} scale
  1814. * A scale. Choose from SCALE.MILLISECOND,
  1815. * SCALE.SECOND, SCALE.MINUTE, SCALE.HOUR,
  1816. * SCALE.DAY, SCALE.MONTH, SCALE.YEAR.
  1817. * @param {int} step A step size, by default 1. Choose for
  1818. * example 1, 2, 5, or 10.
  1819. */
  1820. links.Timeline.prototype.setScale = function(scale, step) {
  1821. this.step.setScale(scale, step);
  1822. this.redrawFrame();
  1823. };
  1824. /**
  1825. * Enable or disable autoscaling
  1826. * @param {boolean} enable If true or not defined, autoscaling is enabled.
  1827. * If false, autoscaling is disabled.
  1828. */
  1829. links.Timeline.prototype.setAutoScale = function(enable) {
  1830. this.step.setAutoScale(enable);
  1831. this.redrawFrame();
  1832. };
  1833. /**
  1834. * Redraw the timeline
  1835. * Reloads the (linked) data table and redraws the timeline when resized.
  1836. * See also the method checkResize
  1837. */
  1838. links.Timeline.prototype.redraw = function() {
  1839. this.setData(this.data);
  1840. };
  1841. /**
  1842. * Check if the timeline is resized, and if so, redraw the timeline.
  1843. * Useful when the webpage is resized.
  1844. */
  1845. links.Timeline.prototype.checkResize = function() {
  1846. var resized = this.recalcSize();
  1847. if (resized) {
  1848. this.redrawFrame();
  1849. }
  1850. };
  1851. /**
  1852. * Recursively retrieve all image urls from the images located inside a given
  1853. * HTML element
  1854. * @param {HTMLElement} elem
  1855. * @param {String[]} urls Urls will be added here (no duplicates)
  1856. */
  1857. links.Timeline.filterImageUrls = function(elem, urls) {
  1858. var child = elem.firstChild;
  1859. while (child) {
  1860. if (child.tagName == 'IMG') {
  1861. var url = child.src;
  1862. if (urls.indexOf(url) == -1) {
  1863. urls.push(url);
  1864. }
  1865. }
  1866. links.Timeline.filterImageUrls(child, urls);
  1867. child = child.nextSibling;
  1868. }
  1869. };
  1870. /**
  1871. * Recalculate the sizes of all frames, groups, items, axis
  1872. * After recalcSize() is executed, the Timeline should be redrawn normally
  1873. *
  1874. * @return {boolean} resized Returns true when the timeline has been resized
  1875. */
  1876. links.Timeline.prototype.recalcSize = function() {
  1877. var resized = false;
  1878. var timeline = this,
  1879. size = this.size,
  1880. options = this.options,
  1881. axisOnTop = options.axisOnTop,
  1882. dom = this.dom,
  1883. axis = dom.axis,
  1884. groups = this.groups,
  1885. labels = dom.groups.labels,
  1886. items = this.items;
  1887. var groupsWidth = size.groupsWidth,
  1888. characterMinorWidth = axis.characterMinor ? axis.characterMinor.clientWidth : 0,
  1889. characterMinorHeight = axis.characterMinor ? axis.characterMinor.clientHeight : 0,
  1890. characterMajorWidth = axis.characterMajor ? axis.characterMajor.clientWidth : 0,
  1891. characterMajorHeight = axis.characterMajor ? axis.characterMajor.clientHeight : 0,
  1892. axisHeight = characterMinorHeight + (options.showMajorLabels ? characterMajorHeight : 0),
  1893. actualHeight = size.actualHeight || axisHeight;
  1894. // TODO: move checking for loaded items when creating the dom
  1895. if (size.dataChanged) {
  1896. // retrieve all image sources from the items, and set a callback once
  1897. // all images are retrieved
  1898. var urls = [];
  1899. for (var i = 0, iMax = items.length; i < iMax; i++) {
  1900. var item = items[i],
  1901. domItem = item.dom;
  1902. if (domItem) {
  1903. links.Timeline.filterImageUrls(domItem, urls);
  1904. }
  1905. }
  1906. if (urls.length) {
  1907. for (var i = 0; i < urls.length; i++) {
  1908. var url = urls[i];
  1909. var callback = function (url) {
  1910. timeline.redraw();
  1911. };
  1912. var sendCallbackWhenAlreadyLoaded = false;
  1913. links.imageloader.load(url, callback, sendCallbackWhenAlreadyLoaded);
  1914. }
  1915. }
  1916. }
  1917. // check sizes of the items and groups (width and height) when the data is changed
  1918. if (size.dataChanged) { // TODO: always calculate the size of an item?
  1919. //if (true) {
  1920. groupsWidth = 0;
  1921. // loop through all groups to get the maximum width and the heights
  1922. for (var i = 0, iMax = labels.length; i < iMax; i++) {
  1923. var group = groups[i];
  1924. group.width = labels[i].clientWidth;
  1925. group.height = labels[i].clientHeight;
  1926. group.labelHeight = group.height;
  1927. groupsWidth = Math.max(groupsWidth, group.width);
  1928. }
  1929. // loop through the width and height of all items
  1930. for (var i = 0, iMax = items.length; i < iMax; i++) {
  1931. var item = items[i],
  1932. domItem = item.dom,
  1933. group = item.group;
  1934. var width = domItem ? domItem.clientWidth : 0;
  1935. var height = domItem ? domItem.clientHeight : 0;
  1936. resized = resized || (item.width != width);
  1937. resized = resized || (item.height != height);
  1938. item.width = width;
  1939. item.height = height;
  1940. //item.borderWidth = (domItem.offsetWidth - domItem.clientWidth - 2) / 2; // TODO: borderWidth
  1941. switch (item.type) {
  1942. case 'range':
  1943. break;
  1944. case 'box':
  1945. item.dotHeight = domItem.dot.offsetHeight;
  1946. item.dotWidth = domItem.dot.offsetWidth;
  1947. item.lineWidth = domItem.line.offsetWidth;
  1948. break;
  1949. case 'dot':
  1950. item.dotHeight = domItem.dot.offsetHeight;
  1951. item.dotWidth = domItem.dot.offsetWidth;
  1952. item.contentHeight = domItem.content.offsetHeight;
  1953. break;
  1954. }
  1955. if (group) {
  1956. group.height = group.height ? Math.max(group.height, item.height) : item.height;
  1957. }
  1958. }
  1959. // calculate the actual height of the timeline (needed for auto sizing
  1960. // the timeline)
  1961. actualHeight = axisHeight + 2 * options.eventMarginAxis;
  1962. for (var i = 0, iMax = groups.length; i < iMax; i++) {
  1963. actualHeight += groups[i].height + options.eventMargin;
  1964. }
  1965. }
  1966. // calculate actual height of the timeline when there are no groups
  1967. // but stacked items
  1968. if (groups.length == 0 && options.autoHeight) {
  1969. var min = 0,
  1970. max = 0;
  1971. if (this.animation && this.animation.finalItems) {
  1972. // adjust the offset of all finalItems when the actualHeight has been changed
  1973. var finalItems = this.animation.finalItems,
  1974. finalItem = finalItems[0];
  1975. if (finalItem && finalItem.top) {
  1976. min = finalItem.top,
  1977. max = finalItem.top + finalItem.height;
  1978. }
  1979. for (var i = 1, iMax = finalItems.length; i < iMax; i++) {
  1980. finalItem = finalItems[i];
  1981. min = Math.min(min, finalItem.top);
  1982. max = Math.max(max, finalItem.top + finalItem.height);
  1983. }
  1984. }
  1985. else {
  1986. var item = items[0];
  1987. if (item && item.top) {
  1988. min = item.top,
  1989. max = item.top + item.height;
  1990. }
  1991. for (var i = 1, iMax = items.length; i < iMax; i++) {
  1992. var item = items[i];
  1993. if (item.top) {
  1994. min = Math.min(min, item.top);
  1995. max = Math.max(max, (item.top + item.height));
  1996. }
  1997. }
  1998. }
  1999. actualHeight = (max - min) + 2 * options.eventMarginAxis + axisHeight;
  2000. if (size.actualHeight != actualHeight && options.autoHeight && !options.axisOnTop) {
  2001. // adjust the offset of all items when the actualHeight has been changed
  2002. var diff = actualHeight - size.actualHeight;
  2003. if (this.animation && this.animation.finalItems) {
  2004. var finalItems = this.animation.finalItems;
  2005. for (var i = 0, iMax = finalItems.length; i < iMax; i++) {
  2006. finalItems[i].top += diff;
  2007. finalItems[i].item.top += diff;
  2008. }
  2009. }
  2010. else {
  2011. for (var i = 0, iMax = items.length; i < iMax; i++) {
  2012. items[i].top += diff;
  2013. }
  2014. }
  2015. }
  2016. }
  2017. // now the heights of the elements are known, we can calculate the the
  2018. // width and height of frame and axis and content
  2019. // Note: IE7 has issues with giving frame.clientWidth, therefore I use offsetWidth instead
  2020. var frameWidth = dom.frame ? dom.frame.offsetWidth : 0,
  2021. frameHeight = Math.max(options.autoHeight ?
  2022. actualHeight : (dom.frame ? dom.frame.clientHeight : 0),
  2023. options.minHeight),
  2024. axisTop = axisOnTop ? 0 : frameHeight - axisHeight,
  2025. axisLine = axisOnTop ? axisHeight : axisTop,
  2026. itemsTop = axisOnTop ? axisHeight : 0,
  2027. contentHeight = Math.max(frameHeight - axisHeight, 0);
  2028. if (options.groupsWidth !== undefined) {
  2029. groupsWidth = dom.groups.frame ? dom.groups.frame.clientWidth : 0;
  2030. }
  2031. var groupsLeft = options.groupsOnRight ? frameWidth - groupsWidth : 0;
  2032. if (size.dataChanged) {
  2033. // calculate top positions of the group labels and lines
  2034. var eventMargin = options.eventMargin,
  2035. top = axisOnTop ?
  2036. options.eventMarginAxis + eventMargin/2 :
  2037. contentHeight - options.eventMarginAxis + eventMargin/2;
  2038. for (var i = 0, iMax = groups.length; i < iMax; i++) {
  2039. var group = groups[i];
  2040. if (axisOnTop) {
  2041. group.top = top;
  2042. group.labelTop = top + axisHeight + (group.height - group.labelHeight) / 2;
  2043. group.lineTop = top + axisHeight + group.height + eventMargin/2;
  2044. top += group.height + eventMargin;
  2045. }
  2046. else {
  2047. top -= group.height + eventMargin;
  2048. group.top = top;
  2049. group.labelTop = top + (group.height - group.labelHeight) / 2;
  2050. group.lineTop = top - eventMargin/2;
  2051. }
  2052. }
  2053. // calculate top position of the items
  2054. for (var i = 0, iMax = items.length; i < iMax; i++) {
  2055. var item = items[i],
  2056. group = item.group;
  2057. if (group) {
  2058. item.top = group.top;
  2059. }
  2060. }
  2061. resized = true;
  2062. }
  2063. resized = resized || (size.groupsWidth !== groupsWidth);
  2064. resized = resized || (size.groupsLeft !== groupsLeft);
  2065. resized = resized || (size.actualHeight !== actualHeight);
  2066. size.groupsWidth = groupsWidth;
  2067. size.groupsLeft = groupsLeft;
  2068. size.actualHeight = actualHeight;
  2069. resized = resized || (size.frameWidth !== frameWidth);
  2070. resized = resized || (size.frameHeight !== frameHeight);
  2071. size.frameWidth = frameWidth;
  2072. size.frameHeight = frameHeight;
  2073. resized = resized || (size.groupsWidth !== groupsWidth);
  2074. size.groupsWidth = groupsWidth;
  2075. size.contentLeft = options.groupsOnRight ? 0 : groupsWidth;
  2076. size.contentWidth = Math.max(frameWidth - groupsWidth, 0);
  2077. size.contentHeight = contentHeight;
  2078. resized = resized || (size.axis.top !== axisTop);
  2079. resized = resized || (size.axis.line !== axisLine);
  2080. resized = resized || (size.axis.height !== axisHeight);
  2081. resized = resized || (size.items.top !== itemsTop);
  2082. size.axis.top = axisTop;
  2083. size.axis.line = axisLine;
  2084. size.axis.height = axisHeight;
  2085. size.axis.labelMajorTop = options.axisOnTop ? 0 : axisLine + characterMinorHeight;
  2086. size.axis.labelMinorTop = options.axisOnTop ?
  2087. (options.showMajorLabels ? characterMajorHeight : 0) :
  2088. axisLine;
  2089. size.axis.lineMinorTop = options.axisOnTop ? size.axis.labelMinorTop : 0;
  2090. size.axis.lineMinorHeight = options.showMajorLabels ?
  2091. frameHeight - characterMajorHeight:
  2092. frameHeight;
  2093. size.axis.lineMinorWidth = dom.axis.minorLines.length ?
  2094. dom.axis.minorLines[0].offsetWidth : 1;
  2095. size.axis.lineMajorWidth = dom.axis.majorLines.length ?
  2096. dom.axis.majorLines[0].offsetWidth : 1;
  2097. size.items.top = itemsTop;
  2098. resized = resized || (size.axis.characterMinorWidth !== characterMinorWidth);
  2099. resized = resized || (size.axis.characterMinorHeight !== characterMinorHeight);
  2100. resized = resized || (size.axis.characterMajorWidth !== characterMajorWidth);
  2101. resized = resized || (size.axis.characterMajorHeight !== characterMajorHeight);
  2102. size.axis.characterMinorWidth = characterMinorWidth;
  2103. size.axis.characterMinorHeight = characterMinorHeight;
  2104. size.axis.characterMajorWidth = characterMajorWidth;
  2105. size.axis.characterMajorHeight = characterMajorHeight;
  2106. // conversion factors can be changed when width of the Timeline is changed,
  2107. // and when start or end are changed
  2108. this.recalcConversion();
  2109. return resized;
  2110. };
  2111. /**
  2112. * Calculate the factor and offset to convert a position on screen to the
  2113. * corresponding date and vice versa.
  2114. * After the method calcConversionFactor is executed once, the methods screenToTime and
  2115. * timeToScreen can be used.
  2116. */
  2117. links.Timeline.prototype.recalcConversion = function() {
  2118. this.conversion.offset = parseFloat(this.start.valueOf());
  2119. this.conversion.factor = parseFloat(this.size.contentWidth) /
  2120. parseFloat(this.end.valueOf() - this.start.valueOf());
  2121. };
  2122. /**
  2123. * Convert a position on screen (pixels) to a datetime
  2124. * Before this method can be used, the method calcConversionFactor must be
  2125. * executed once.
  2126. * @param {int} x Position on the screen in pixels
  2127. * @return {Date} time The datetime the corresponds with given position x
  2128. */
  2129. links.Timeline.prototype.screenToTime = function(x) {
  2130. var conversion = this.conversion,
  2131. time = new Date(parseFloat(x) / conversion.factor + conversion.offset);
  2132. return time;
  2133. };
  2134. /**
  2135. * Convert a datetime (Date object) into a position on the screen
  2136. * Before this method can be used, the method calcConversionFactor must be
  2137. * executed once.
  2138. * @param {Date} time A date
  2139. * @return {int} x The position on the screen in pixels which corresponds
  2140. * with the given date.
  2141. */
  2142. links.Timeline.prototype.timeToScreen = function(time) {
  2143. var conversion = this.conversion;
  2144. var x = (time.valueOf() - conversion.offset) * conversion.factor;
  2145. return x;
  2146. };
  2147. /**
  2148. * Event handler for touchstart event on mobile devices
  2149. */
  2150. links.Timeline.prototype.onTouchStart = function(event) {
  2151. var params = this.eventParams,
  2152. dom = this.dom,
  2153. me = this;
  2154. if (params.touchDown) {
  2155. // if already moving, return
  2156. return;
  2157. }
  2158. params.touchDown = true;
  2159. params.zoomed = false;
  2160. this.onMouseDown(event);
  2161. if (!params.onTouchMove) {
  2162. params.onTouchMove = function (event) {me.onTouchMove(event);};
  2163. links.Timeline.addEventListener(document, "touchmove", params.onTouchMove);
  2164. }
  2165. if (!params.onTouchEnd) {
  2166. params.onTouchEnd = function (event) {me.onTouchEnd(event);};
  2167. links.Timeline.addEventListener(document, "touchend", params.onTouchEnd);
  2168. }
  2169. };
  2170. /**
  2171. * Event handler for touchmove event on mobile devices
  2172. */
  2173. links.Timeline.prototype.onTouchMove = function(event) {
  2174. var params = this.eventParams;
  2175. if (event.scale && event.scale !== 1) {
  2176. params.zoomed = true;
  2177. }
  2178. if (!params.zoomed) {
  2179. // move
  2180. this.onMouseMove(event);
  2181. }
  2182. else {
  2183. if (this.options.zoomable) {
  2184. // pinch
  2185. // TODO: pinch only supported on iPhone/iPad. Create something manually for Android?
  2186. params.zoomed = true;
  2187. var scale = event.scale,
  2188. oldWidth = (params.end.valueOf() - params.start.valueOf()),
  2189. newWidth = oldWidth / scale,
  2190. diff = newWidth - oldWidth,
  2191. start = new Date(parseInt(params.start.valueOf() - diff/2)),
  2192. end = new Date(parseInt(params.end.valueOf() + diff/2));
  2193. // TODO: determine zoom-around-date from touch positions?
  2194. this.setVisibleChartRange(start, end);
  2195. timeline.trigger("rangechange");
  2196. links.Timeline.preventDefault(event);
  2197. }
  2198. }
  2199. };
  2200. /**
  2201. * Event handler for touchend event on mobile devices
  2202. */
  2203. links.Timeline.prototype.onTouchEnd = function(event) {
  2204. var params = this.eventParams;
  2205. params.touchDown = false;
  2206. if (params.zoomed) {
  2207. timeline.trigger("rangechanged");
  2208. }
  2209. if (params.onTouchMove) {
  2210. links.Timeline.removeEventListener(document, "touchmove", params.onTouchMove);
  2211. delete params.onTouchMove;
  2212. }
  2213. if (params.onTouchEnd) {
  2214. links.Timeline.removeEventListener(document, "touchend", params.onTouchEnd);
  2215. delete params.onTouchEnd;
  2216. }
  2217. this.onMouseUp(event);
  2218. };
  2219. /**
  2220. * Start a moving operation inside the provided parent element
  2221. * @param {event} event The event that occurred (required for
  2222. * retrieving the mouse position)
  2223. */
  2224. links.Timeline.prototype.onMouseDown = function(event) {
  2225. event = event || window.event;
  2226. var params = this.eventParams,
  2227. options = this.options,
  2228. dom = this.dom;
  2229. // only react on left mouse button down
  2230. var leftButtonDown = event.which ? (event.which == 1) : (event.button == 1);
  2231. if (!leftButtonDown && !params.touchDown) {
  2232. return;
  2233. }
  2234. // check if frame is not resized (causing a mismatch with the end Date)
  2235. this.recalcSize();
  2236. // get mouse position
  2237. if (!params.touchDown) {
  2238. params.mouseX = event.clientX;
  2239. params.mouseY = event.clientY;
  2240. }
  2241. else {
  2242. params.mouseX = event.targetTouches[0].clientX;
  2243. params.mouseY = event.targetTouches[0].clientY;
  2244. }
  2245. if (params.mouseX === undefined) {params.mouseX = 0;}
  2246. if (params.mouseY === undefined) {params.mouseY = 0;}
  2247. params.frameLeft = links.Timeline.getAbsoluteLeft(this.dom.content);
  2248. params.frameTop = links.Timeline.getAbsoluteTop(this.dom.content);
  2249. params.previousLeft = 0;
  2250. params.previousOffset = 0;
  2251. params.moved = false;
  2252. params.start = new Date(this.start);
  2253. params.end = new Date(this.end);
  2254. params.target = links.Timeline.getTarget(event);
  2255. params.itemDragLeft = (params.target === this.dom.items.dragLeft);
  2256. params.itemDragRight = (params.target === this.dom.items.dragRight);
  2257. if (params.itemDragLeft || params.itemDragRight) {
  2258. params.itemIndex = this.selection ? this.selection.index : undefined;
  2259. }
  2260. else {
  2261. params.itemIndex = this.getItemIndex(params.target);
  2262. }
  2263. params.customTime = (params.target === dom.customTime ||
  2264. params.target.parentNode === dom.customTime) ?
  2265. this.customTime :
  2266. undefined;
  2267. params.addItem = (options.editable && event.ctrlKey);
  2268. if (params.addItem) {
  2269. // create a new event at the current mouse position
  2270. var x = params.mouseX - params.frameLeft;
  2271. var y = params.mouseY - params.frameTop;
  2272. var xstart = this.screenToTime(x);
  2273. if (options.snapEvents) {
  2274. this.step.snap(xstart);
  2275. }
  2276. var xend = new Date(xstart);
  2277. var content = "New";
  2278. var group = this.getGroupFromHeight(y);
  2279. this.addItem({
  2280. 'start': xstart,
  2281. 'end': xend,
  2282. 'content': content,
  2283. 'group': group.content
  2284. });
  2285. params.itemIndex = (this.items.length - 1);
  2286. this.selectItem(params.itemIndex);
  2287. params.itemDragRight = true;
  2288. }
  2289. params.editItem = options.editable ? this.isSelected(params.itemIndex) : undefined;
  2290. if (params.editItem) {
  2291. var item = this.items[params.itemIndex];
  2292. params.itemStart = item.start;
  2293. params.itemEnd = item.end;
  2294. params.itemType = item.type;
  2295. if (params.itemType == 'range') {
  2296. params.itemLeft = this.timeToScreen(item.start);
  2297. params.itemRight = this.timeToScreen(item.end);
  2298. }
  2299. else {
  2300. params.itemLeft = this.timeToScreen(item.start);
  2301. }
  2302. }
  2303. else {
  2304. this.dom.frame.style.cursor = 'move';
  2305. }
  2306. if (!params.touchDown) {
  2307. // add event listeners to handle moving the contents
  2308. // we store the function onmousemove and onmouseup in the timeline, so we can
  2309. // remove the eventlisteners lateron in the function mouseUp()
  2310. var me = this;
  2311. if (!params.onMouseMove) {
  2312. params.onMouseMove = function (event) {me.onMouseMove(event);};
  2313. links.Timeline.addEventListener(document, "mousemove", params.onMouseMove);
  2314. }
  2315. if (!params.onMouseUp) {
  2316. params.onMouseUp = function (event) {me.onMouseUp(event);};
  2317. links.Timeline.addEventListener(document, "mouseup", params.onMouseUp);
  2318. }
  2319. links.Timeline.preventDefault(event);
  2320. }
  2321. };
  2322. /**
  2323. * Perform moving operating.
  2324. * This function activated from within the funcion links.Timeline.onMouseDown().
  2325. * @param {event} event Well, eehh, the event
  2326. */
  2327. links.Timeline.prototype.onMouseMove = function (event) {
  2328. event = event || window.event;
  2329. var params = this.eventParams,
  2330. size = this.size,
  2331. dom = this.dom,
  2332. options = this.options;
  2333. // calculate change in mouse position
  2334. var mouseX, mouseY;
  2335. if (!params.touchDown) {
  2336. mouseX = event.clientX;
  2337. mouseY = event.clientY;
  2338. }
  2339. else {
  2340. mouseX = event.targetTouches[0].clientX;
  2341. mouseY = event.targetTouches[0].clientY;
  2342. }
  2343. if (mouseX === undefined) {mouseX = 0;}
  2344. if (mouseY === undefined) {mouseY = 0;}
  2345. if (params.mouseX === undefined) {
  2346. params.mouseX = mouseX;
  2347. }
  2348. if (params.mouseY === undefined) {
  2349. params.mouseY = mouseY;
  2350. }
  2351. var diffX = parseFloat(mouseX) - params.mouseX;
  2352. var diffY = parseFloat(mouseY) - params.mouseY;
  2353. params.moved = true;
  2354. if (params.customTime) {
  2355. var x = this.timeToScreen(params.customTime);
  2356. var xnew = x + diffX;
  2357. this.customTime = this.screenToTime(xnew);
  2358. this.redrawCustomTime();
  2359. // fire a timechange event
  2360. this.trigger('timechange');
  2361. }
  2362. else if (params.editItem) {
  2363. var item = this.items[params.itemIndex],
  2364. domItem = item.dom,
  2365. left,
  2366. right;
  2367. if (params.itemDragLeft) {
  2368. // move the start of the item
  2369. left = params.itemLeft + diffX;
  2370. right = params.itemRight;
  2371. item.start = this.screenToTime(left);
  2372. if (options.snapEvents) {
  2373. this.step.snap(item.start);
  2374. left = this.timeToScreen(item.start);
  2375. }
  2376. if (left > right) {
  2377. left = right;
  2378. item.start = this.screenToTime(left);
  2379. }
  2380. }
  2381. else if (params.itemDragRight) {
  2382. // move the end of the item
  2383. left = params.itemLeft;
  2384. right = params.itemRight + diffX;
  2385. item.end = this.screenToTime(right);
  2386. if (options.snapEvents) {
  2387. this.step.snap(item.end);
  2388. right = this.timeToScreen(item.end);
  2389. }
  2390. if (right < left) {
  2391. right = left;
  2392. item.end = this.screenToTime(right);
  2393. }
  2394. }
  2395. else {
  2396. // move the item
  2397. left = params.itemLeft + diffX;
  2398. item.start = this.screenToTime(left);
  2399. if (options.snapEvents) {
  2400. this.step.snap(item.start);
  2401. left = this.timeToScreen(item.start);
  2402. }
  2403. if (item.end) {
  2404. right = left + (params.itemRight - params.itemLeft);
  2405. item.end = this.screenToTime(right);
  2406. }
  2407. }
  2408. this.repositionItem(item, left, right);
  2409. if (this.groups.length == 0) {
  2410. // TODO: does not work well in FF, forces redraw with every mouse move it seems
  2411. this.stackEvents(options.animate);
  2412. if (!options.animate) {
  2413. this.redrawFrame();
  2414. }
  2415. // Note: when animate==true, no redraw is needed here, its done by stackEvents animation
  2416. }
  2417. else {
  2418. // move item from one group to another when needed
  2419. if (options.groupsChangeable) {
  2420. var y = mouseY - params.frameTop;
  2421. var group = this.getGroupFromHeight(y);
  2422. if (item.group !== group) {
  2423. // move item to the other group
  2424. //item.group = group;
  2425. var index = this.items.indexOf(item);
  2426. this.changeItem(index, {'group': group.content});
  2427. item.top = group.top;
  2428. this.repositionItem(item);
  2429. }
  2430. }
  2431. }
  2432. this.redrawDeleteButton();
  2433. this.redrawDragAreas();
  2434. }
  2435. else if (options.moveable) {
  2436. var interval = (params.end.valueOf() - params.start.valueOf());
  2437. var diffMillisecs = Math.round(parseFloat(-diffX) / size.contentWidth * interval);
  2438. var newStart = new Date(params.start.valueOf() + diffMillisecs);
  2439. var newEnd = new Date(params.end.valueOf() + diffMillisecs);
  2440. this.applyRange(newStart, newEnd);
  2441. // if the applied range is moved due to a fixed min or max,
  2442. // change the diffMillisecs accordingly
  2443. var appliedDiff = (this.start.valueOf() - newStart.valueOf());
  2444. if (appliedDiff) {
  2445. diffMillisecs += appliedDiff;
  2446. }
  2447. this.recalcConversion();
  2448. // move the items by changing the left position of their frame.
  2449. // this is much faster than repositioning all elements individually via the
  2450. // redrawFrame() function (which is done once at mouseup)
  2451. // note that we round diffX to prevent wrong positioning on millisecond scale
  2452. var previousLeft = params.previousLeft || 0;
  2453. var currentLeft = parseFloat(dom.items.frame.style.left) || 0;
  2454. var previousOffset = params.previousOffset || 0;
  2455. var frameOffset = previousOffset + (currentLeft - previousLeft);
  2456. var frameLeft = -diffMillisecs / interval * size.contentWidth + frameOffset;
  2457. params.previousOffset = frameOffset;
  2458. params.previousLeft = frameLeft;
  2459. dom.items.frame.style.left = (frameLeft) + "px";
  2460. this.redrawCurrentTime();
  2461. this.redrawCustomTime();
  2462. this.redrawAxis();
  2463. // fire a rangechange event
  2464. this.trigger('rangechange');
  2465. }
  2466. links.Timeline.preventDefault(event);
  2467. };
  2468. /**
  2469. * Stop moving operating.
  2470. * This function activated from within the funcion links.Timeline.onMouseDown().
  2471. * @param {event} event The event
  2472. */
  2473. links.Timeline.prototype.onMouseUp = function (event) {
  2474. var params = this.eventParams,
  2475. options = this.options;
  2476. event = event || window.event;
  2477. this.dom.frame.style.cursor = 'auto';
  2478. // remove event listeners here, important for Safari
  2479. if (params.onMouseMove) {
  2480. links.Timeline.removeEventListener(document, "mousemove", params.onMouseMove);
  2481. delete params.onMouseMove;
  2482. }
  2483. if (params.onMouseUp) {
  2484. links.Timeline.removeEventListener(document, "mouseup", params.onMouseUp);
  2485. delete params.onMouseUp;
  2486. }
  2487. //links.Timeline.preventDefault(event);
  2488. if (params.customTime) {
  2489. // fire a timechanged event
  2490. this.trigger('timechanged');
  2491. }
  2492. else if (params.editItem) {
  2493. var item = this.items[params.itemIndex];
  2494. if (params.moved || params.addItem) {
  2495. this.applyChange = true;
  2496. this.applyAdd = true;
  2497. this.updateData(params.itemIndex, {
  2498. 'start': item.start,
  2499. 'end': item.end
  2500. });
  2501. // fire an add or change event.
  2502. // Note that the change can be canceled from within an event listener if
  2503. // this listener calls the method cancelChange().
  2504. this.trigger(params.addItem ? 'add' : 'change');
  2505. if (params.addItem) {
  2506. if (this.applyAdd) {
  2507. this.updateData(params.itemIndex, {
  2508. 'start': item.start,
  2509. 'end': item.end,
  2510. 'content': item.content,
  2511. 'group': item.group ? item.group.content : undefined
  2512. });
  2513. }
  2514. else {
  2515. // undo an add
  2516. this.deleteItem(params.itemIndex);
  2517. }
  2518. }
  2519. else {
  2520. if (this.applyChange) {
  2521. this.updateData(params.itemIndex, {
  2522. 'start': item.start,
  2523. 'end': item.end
  2524. });
  2525. }
  2526. else {
  2527. // undo a change
  2528. delete this.applyChange;
  2529. delete this.applyAdd;
  2530. var item = this.items[params.itemIndex],
  2531. domItem = item.dom;
  2532. item.start = params.itemStart;
  2533. item.end = params.itemEnd;
  2534. this.repositionItem(item, params.itemLeft, params.itemRight);
  2535. }
  2536. }
  2537. this.recalcSize();
  2538. this.stackEvents(options.animate);
  2539. if (!options.animate) {
  2540. this.redrawFrame();
  2541. }
  2542. this.redrawDeleteButton();
  2543. this.redrawDragAreas();
  2544. }
  2545. }
  2546. else {
  2547. if (!params.moved && !params.zoomed) {
  2548. // mouse did not move -> user has selected an item
  2549. if (options.editable && (params.target === this.dom.items.deleteButton)) {
  2550. // delete item
  2551. if (this.selection) {
  2552. this.confirmDeleteItem(this.selection.index);
  2553. }
  2554. this.redrawFrame();
  2555. }
  2556. else if (options.selectable) {
  2557. // select/unselect item
  2558. if (params.itemIndex !== undefined) {
  2559. if (!this.isSelected(params.itemIndex)) {
  2560. this.selectItem(params.itemIndex);
  2561. this.redrawDeleteButton();
  2562. this.redrawDragAreas();
  2563. this.trigger('select');
  2564. }
  2565. }
  2566. else {
  2567. this.unselectItem();
  2568. this.redrawDeleteButton();
  2569. this.redrawDragAreas();
  2570. }
  2571. }
  2572. }
  2573. else {
  2574. // timeline is moved
  2575. this.redrawFrame();
  2576. if ((params.moved && options.moveable) || (params.zoomed && options.zoomable) ) {
  2577. // fire a rangechanged event
  2578. this.trigger('rangechanged');
  2579. }
  2580. }
  2581. }
  2582. };
  2583. /**
  2584. * Double click event occurred for an item
  2585. * @param {event} event
  2586. */
  2587. links.Timeline.prototype.onDblClick = function (event) {
  2588. var params = this.eventParams,
  2589. options = this.options,
  2590. dom = this.dom,
  2591. size = this.size;
  2592. event = event || window.event;
  2593. if (!options.editable) {
  2594. return;
  2595. }
  2596. if (params.itemIndex !== undefined) {
  2597. // fire the edit event
  2598. this.trigger('edit');
  2599. }
  2600. else {
  2601. // create a new item
  2602. var x = event.clientX - links.Timeline.getAbsoluteLeft(dom.content);
  2603. var y = event.clientY - links.Timeline.getAbsoluteTop(dom.content);
  2604. // create a new event at the current mouse position
  2605. var xstart = this.screenToTime(x);
  2606. var xend = this.screenToTime(x + size.frameWidth / 10); // add 10% of timeline width
  2607. if (options.snapEvents) {
  2608. this.step.snap(xstart);
  2609. this.step.snap(xend);
  2610. }
  2611. var content = "New";
  2612. var group = this.getGroupFromHeight(y); // (group may be undefined)
  2613. this.addItem({
  2614. 'start': xstart,
  2615. 'end': xend,
  2616. 'content': content,
  2617. 'group': group.content
  2618. });
  2619. params.itemIndex = (this.items.length - 1);
  2620. this.selectItem(params.itemIndex);
  2621. this.applyAdd = true;
  2622. // fire an add event.
  2623. // Note that the change can be canceled from within an event listener if
  2624. // this listener calls the method cancelAdd().
  2625. this.trigger('add');
  2626. if (!this.applyAdd) {
  2627. // undo an add
  2628. this.deleteItem(params.itemIndex);
  2629. }
  2630. this.redrawDeleteButton();
  2631. this.redrawDragAreas();
  2632. }
  2633. links.Timeline.preventDefault(event);
  2634. };
  2635. /**
  2636. * Event handler for mouse wheel event, used to zoom the timeline
  2637. * Code from http://adomas.org/javascript-mouse-wheel/
  2638. * @param {event} event The event
  2639. */
  2640. links.Timeline.prototype.onMouseWheel = function(event) {
  2641. if (!this.options.zoomable)
  2642. return;
  2643. if (!event) { /* For IE. */
  2644. event = window.event;
  2645. }
  2646. // retrieve delta
  2647. var delta = 0;
  2648. if (event.wheelDelta) { /* IE/Opera. */
  2649. delta = event.wheelDelta/120;
  2650. } else if (event.detail) { /* Mozilla case. */
  2651. // In Mozilla, sign of delta is different than in IE.
  2652. // Also, delta is multiple of 3.
  2653. delta = -event.detail/3;
  2654. }
  2655. // If delta is nonzero, handle it.
  2656. // Basically, delta is now positive if wheel was scrolled up,
  2657. // and negative, if wheel was scrolled down.
  2658. if (delta) {
  2659. // TODO: on FireFox, the window is not redrawn within repeated scroll-events
  2660. // -> use a delayed redraw? Make a zoom queue?
  2661. var timeline = this;
  2662. var zoom = function () {
  2663. // check if frame is not resized (causing a mismatch with the end date)
  2664. timeline.recalcSize();
  2665. // perform the zoom action. Delta is normally 1 or -1
  2666. var zoomFactor = delta / 5.0;
  2667. var frameLeft = links.Timeline.getAbsoluteLeft(timeline.dom.content);
  2668. var zoomAroundDate =
  2669. (event.clientX != undefined && frameLeft != undefined) ?
  2670. timeline.screenToTime(event.clientX - frameLeft) :
  2671. undefined;
  2672. timeline.zoom(zoomFactor, zoomAroundDate);
  2673. // fire a rangechange and a rangechanged event
  2674. timeline.trigger("rangechange");
  2675. timeline.trigger("rangechanged");
  2676. /* TODO: smooth scrolling on FF
  2677. timeline.zooming = false;
  2678. if (timeline.zoomingQueue) {
  2679. setTimeout(timeline.zoomingQueue, 100);
  2680. timeline.zoomingQueue = undefined;
  2681. }
  2682. timeline.zoomCount = (timeline.zoomCount || 0) + 1;
  2683. //console.log('zoomCount', timeline.zoomCount)
  2684. */
  2685. };
  2686. zoom();
  2687. /* TODO: smooth scrolling on FF
  2688. if (!timeline.zooming || true) {
  2689. timeline.zooming = true;
  2690. setTimeout(zoom, 100);
  2691. }
  2692. else {
  2693. timeline.zoomingQueue = zoom;
  2694. }
  2695. //*/
  2696. }
  2697. // Prevent default actions caused by mouse wheel.
  2698. // That might be ugly, but we handle scrolls somehow
  2699. // anyway, so don't bother here...
  2700. links.Timeline.preventDefault(event);
  2701. };
  2702. /**
  2703. * Zoom the timeline the given zoomfactor in or out. Start and end date will
  2704. * be adjusted, and the timeline will be redrawn. You can optionally give a
  2705. * date around which to zoom.
  2706. * For example, try zoomfactor = 0.1 or -0.1
  2707. * @param {Number} zoomFactor Zooming amount. Positive value will zoom in,
  2708. * negative value will zoom out
  2709. * @param {Date} zoomAroundDate Date around which will be zoomed. Optional
  2710. */
  2711. links.Timeline.prototype.zoom = function(zoomFactor, zoomAroundDate) {
  2712. // if zoomAroundDate is not provided, take it half between start Date and end Date
  2713. if (zoomAroundDate == undefined) {
  2714. zoomAroundDate = new Date((this.start.valueOf() + this.end.valueOf()) / 2);
  2715. }
  2716. // prevent zoom factor larger than 1 or smaller than -1 (larger than 1 will
  2717. // result in a start>=end )
  2718. if (zoomFactor >= 1) {
  2719. zoomFactor = 0.9;
  2720. }
  2721. if (zoomFactor <= -1) {
  2722. zoomFactor = -0.9;
  2723. }
  2724. // adjust a negative factor such that zooming in with 0.1 equals zooming
  2725. // out with a factor -0.1
  2726. if (zoomFactor < 0) {
  2727. zoomFactor = zoomFactor / (1 + zoomFactor);
  2728. }
  2729. // zoom start Date and end Date relative to the zoomAroundDate
  2730. var startDiff = parseFloat(this.start.valueOf() - zoomAroundDate.valueOf());
  2731. var endDiff = parseFloat(this.end.valueOf() - zoomAroundDate.valueOf());
  2732. // calculate new dates
  2733. var newStart = new Date(this.start.valueOf() - startDiff * zoomFactor);
  2734. var newEnd = new Date(this.end.valueOf() - endDiff * zoomFactor);
  2735. this.applyRange(newStart, newEnd, zoomAroundDate);
  2736. this.recalcSize();
  2737. var animate = this.options.animate ? this.options.animateZoom : false;
  2738. this.stackEvents(animate);
  2739. if (!animate || this.groups.length > 0) {
  2740. this.redrawFrame();
  2741. }
  2742. /* TODO
  2743. else {
  2744. this.redrawFrame();
  2745. this.recalcSize();
  2746. this.stackEvents(animate);
  2747. this.redrawFrame();
  2748. }*/
  2749. };
  2750. /**
  2751. * Move the timeline the given movefactor to the left or right. Start and end
  2752. * date will be adjusted, and the timeline will be redrawn.
  2753. * For example, try moveFactor = 0.1 or -0.1
  2754. * @param {Number} moveFactor Moving amount. Positive value will move right,
  2755. * negative value will move left
  2756. */
  2757. links.Timeline.prototype.move = function(moveFactor) {
  2758. // zoom start Date and end Date relative to the zoomAroundDate
  2759. var diff = parseFloat(this.end.valueOf() - this.start.valueOf());
  2760. // apply new dates
  2761. var newStart = new Date(this.start.valueOf() + diff * moveFactor);
  2762. var newEnd = new Date(this.end.valueOf() + diff * moveFactor);
  2763. this.applyRange(newStart, newEnd);
  2764. this.recalcConversion();
  2765. this.redrawFrame();
  2766. };
  2767. /**
  2768. * Reposition given item
  2769. * @param {Object} item
  2770. * @param {Number} left
  2771. * @param {Number} right
  2772. */
  2773. links.Timeline.prototype.repositionItem = function (item, left, right) {
  2774. var domItem = item.dom;
  2775. switch(item.type) {
  2776. case 'range':
  2777. domItem.style.left = left + "px";
  2778. //domItem.style.width = Math.max(right - left - 2 * item.borderWidth, 1) + "px"; // TODO: borderwidth
  2779. domItem.style.width = Math.max(right - left, 1) + "px";
  2780. break;
  2781. case 'box':
  2782. domItem.style.left = (left - item.width / 2) + "px";
  2783. domItem.line.style.left = (left - item.lineWidth / 2) + "px";
  2784. domItem.dot.style.left = (left - item.dotWidth / 2) + "px";
  2785. break;
  2786. case 'dot':
  2787. domItem.style.left = (left - item.dotWidth / 2) + "px";
  2788. break;
  2789. }
  2790. if (this.groups.length > 0) {
  2791. domItem.style.top = item.top + 'px';
  2792. }
  2793. };
  2794. /**
  2795. * Apply a visible range. The range is limited to feasible maximum and minimum
  2796. * range.
  2797. * @param {Date} start
  2798. * @param {Date} end
  2799. * @param {Date} zoomAroundDate Optional. Date around which will be zoomed.
  2800. */
  2801. links.Timeline.prototype.applyRange = function (start, end, zoomAroundDate) {
  2802. // calculate new start and end value
  2803. var startValue = start.valueOf();
  2804. var endValue = end.valueOf();
  2805. var interval = (endValue - startValue);
  2806. // determine maximum and minimum interval
  2807. var options = this.options;
  2808. var year = 1000 * 60 * 60 * 24 * 365;
  2809. var intervalMin = Number(options.intervalMin) || 10;
  2810. if (intervalMin < 10) {
  2811. intervalMin = 10;
  2812. }
  2813. var intervalMax = Number(options.intervalMax) || 10000 * year;
  2814. if (intervalMax > 10000 * year) {
  2815. intervalMax = 10000 * year;
  2816. }
  2817. if (intervalMax < intervalMin) {
  2818. intervalMax = intervalMin;
  2819. }
  2820. // determine min and max date value
  2821. var min = options.min ? options.min.valueOf() : undefined;
  2822. var max = options.max ? options.max.valueOf() : undefined;
  2823. if (min && max) {
  2824. if (min >= max) {
  2825. // empty range
  2826. var day = 1000 * 60 * 60 * 24;
  2827. max = min + day;
  2828. }
  2829. if (intervalMax > (max - min)) {
  2830. intervalMax = (max - min);
  2831. }
  2832. if (intervalMin > (max - min)) {
  2833. intervalMin = (max - min);
  2834. }
  2835. }
  2836. // prevent empty interval
  2837. if (startValue >= endValue) {
  2838. endValue += 1000 * 60 * 60 * 24;
  2839. }
  2840. // prevent too small scale
  2841. // TODO: IE has problems with milliseconds
  2842. if (interval < intervalMin) {
  2843. var diff = (intervalMin - interval);
  2844. var f = zoomAroundDate ? (zoomAroundDate.valueOf() - startValue) / interval : 0.5;
  2845. startValue -= Math.round(diff * f);
  2846. endValue += Math.round(diff * (1 - f));
  2847. }
  2848. // prevent too large scale
  2849. if (interval > intervalMax) {
  2850. var diff = (interval - intervalMax);
  2851. var f = zoomAroundDate ? (zoomAroundDate.valueOf() - startValue) / interval : 0.5;
  2852. startValue += Math.round(diff * f);
  2853. endValue -= Math.round(diff * (1 - f));
  2854. }
  2855. // prevent to small start date
  2856. if (min) {
  2857. var diff = (startValue - min);
  2858. if (diff < 0) {
  2859. startValue -= diff;
  2860. endValue -= diff;
  2861. }
  2862. }
  2863. // prevent to large end date
  2864. if (max) {
  2865. var diff = (max - endValue);
  2866. if (diff < 0) {
  2867. startValue += diff;
  2868. endValue += diff;
  2869. }
  2870. }
  2871. // apply new dates
  2872. this.start = new Date(startValue);
  2873. this.end = new Date(endValue);
  2874. };
  2875. /**
  2876. * Delete an item after a confirmation.
  2877. * The deletion can be cancelled by executing .cancelDelete() during the
  2878. * triggered event 'delete'.
  2879. * @param {int} index Index of the item to be deleted
  2880. */
  2881. links.Timeline.prototype.confirmDeleteItem = function(index) {
  2882. this.applyDelete = true;
  2883. // select the event to be deleted
  2884. if (!this.isSelected(index)) {
  2885. this.selectItem(index);
  2886. }
  2887. // fire a delete event trigger.
  2888. // Note that the delete event can be canceled from within an event listener if
  2889. // this listener calls the method cancelChange().
  2890. this.trigger('delete');
  2891. if (this.applyDelete) {
  2892. this.deleteItem(index);
  2893. }
  2894. delete this.applyDelete;
  2895. };
  2896. /**
  2897. * Delete an item
  2898. * @param {int} index Index of the item to be deleted
  2899. */
  2900. links.Timeline.prototype.deleteItem = function(index) {
  2901. if (index >= this.items.length) {
  2902. throw "Cannot delete row, index out of range";
  2903. }
  2904. this.unselectItem();
  2905. // actually delete the item
  2906. this.items.splice(index, 1);
  2907. // delete the row in the original data table
  2908. if (this.data) {
  2909. if (google && google.visualization &&
  2910. this.data instanceof google.visualization.DataTable) {
  2911. this.data.removeRow(index);
  2912. }
  2913. else if (links.Timeline.isArray(this.data)) {
  2914. this.data.splice(index, 1);
  2915. }
  2916. else {
  2917. throw "Cannot delete row from data, unknown data type";
  2918. }
  2919. }
  2920. this.size.dataChanged = true;
  2921. this.redrawFrame();
  2922. this.recalcSize();
  2923. this.stackEvents(this.options.animate);
  2924. if (!this.options.animate) {
  2925. this.redrawFrame();
  2926. }
  2927. this.size.dataChanged = false;
  2928. };
  2929. /**
  2930. * Delete all items
  2931. */
  2932. links.Timeline.prototype.deleteAllItems = function() {
  2933. this.unselectItem();
  2934. // delete the loaded data
  2935. this.items = [];
  2936. // delete the groups
  2937. this.deleteGroups();
  2938. // empty original data table
  2939. if (this.data) {
  2940. if (google && google.visualization &&
  2941. this.data instanceof google.visualization.DataTable) {
  2942. this.data.removeRows(0, this.data.getNumberOfRows());
  2943. }
  2944. else if (links.Timeline.isArray(this.data)) {
  2945. this.data.splice(0, this.data.length);
  2946. }
  2947. else {
  2948. throw "Cannot delete row from data, unknown data type";
  2949. }
  2950. }
  2951. this.size.dataChanged = true;
  2952. this.redrawFrame();
  2953. this.recalcSize();
  2954. this.stackEvents(this.options.animate);
  2955. if (!this.options.animate) {
  2956. this.redrawFrame();
  2957. }
  2958. this.size.dataChanged = false;
  2959. };
  2960. /**
  2961. * Find the group from a given height in the timeline
  2962. * @param {Number} height Height in the timeline
  2963. * @param {boolean}
  2964. * @return {Object} group The group object, or undefined if out of range
  2965. */
  2966. links.Timeline.prototype.getGroupFromHeight = function(height) {
  2967. var groups = this.groups,
  2968. options = this.options,
  2969. size = this.size,
  2970. y = height - (options.axisOnTop ? size.axis.height : 0);
  2971. if (groups) {
  2972. var group;
  2973. /* TODO: cleanup
  2974. for (var i = 0, iMax = groups.length; i < iMax; i++) {
  2975. group = groups[i];
  2976. if (y > group.top && y < group.top + group.height) {
  2977. return group;
  2978. }
  2979. }*/
  2980. for (var i = groups.length - 1; i >= 0; i--) {
  2981. group = groups[i];
  2982. if (y > group.top) {
  2983. return group;
  2984. }
  2985. }
  2986. return group; // return the last group
  2987. }
  2988. return undefined;
  2989. };
  2990. /**
  2991. * Retrieve the properties of an item.
  2992. * @param {Number} index
  2993. * @return {Object} properties Object containing item properties:<br>
  2994. * {Date} start (required),
  2995. * {Date} end (optional),
  2996. * {String} content (required),
  2997. * {String} group (optional)
  2998. */
  2999. links.Timeline.prototype.getItem = function (index) {
  3000. if (index >= this.items.length) {
  3001. throw "Cannot get item, index out of range";
  3002. }
  3003. var item = this.items[index];
  3004. var properties = {};
  3005. properties.start = new Date(item.start);
  3006. if (item.end) {
  3007. properties.end = new Date(item.end);
  3008. }
  3009. properties.content = item.content;
  3010. if (item.group) {
  3011. properties.group = item.group.content;
  3012. }
  3013. return properties;
  3014. };
  3015. /**
  3016. * Add a new item.
  3017. * @param {Object} itemData Object containing item properties:<br>
  3018. * {Date} start (required),
  3019. * {Date} end (optional),
  3020. * {String} content (required),
  3021. * {String} group (optional)
  3022. */
  3023. links.Timeline.prototype.addItem = function (itemData) {
  3024. var items = [
  3025. itemData
  3026. ];
  3027. this.addItems(items);
  3028. };
  3029. /**
  3030. * Add new items.
  3031. * @param {Array} items An array containing Objects.
  3032. * The objects must have the following parameters:
  3033. * {Date} start,
  3034. * {Date} end,
  3035. * {String} content with text or HTML code,
  3036. * {String} group
  3037. */
  3038. links.Timeline.prototype.addItems = function (items) {
  3039. var newItems = items,
  3040. curItems = this.items;
  3041. // append the items
  3042. for (var i = 0, iMax = newItems.length; i < iMax; i++) {
  3043. var itemData = items[i];
  3044. this.addGroup(itemData.group);
  3045. curItems.push(this.createItem(itemData));
  3046. var index = curItems.length - 1;
  3047. this.updateData(index, itemData);
  3048. }
  3049. // redraw timeline
  3050. this.size.dataChanged = true;
  3051. this.redrawFrame();
  3052. this.recalcSize();
  3053. this.stackEvents(false);
  3054. this.redrawFrame();
  3055. this.size.dataChanged = false;
  3056. };
  3057. /**
  3058. * Create an item object, containing all needed parameters
  3059. * @param {Object} itemData Object containing parameters start, end
  3060. * content, group.
  3061. * @return {Object} item
  3062. */
  3063. links.Timeline.prototype.createItem = function(itemData) {
  3064. var item = {
  3065. 'start': itemData.start,
  3066. 'end': itemData.end,
  3067. 'content': itemData.content,
  3068. 'type': itemData.end ? 'range' : this.options.style,
  3069. 'group': this.findGroup(itemData.group),
  3070. 'top': 0,
  3071. 'left': 0,
  3072. 'width': 0,
  3073. 'height': 0,
  3074. 'lineWidth' : 0,
  3075. 'dotWidth': 0,
  3076. 'dotHeight': 0
  3077. };
  3078. return item;
  3079. };
  3080. /**
  3081. * Edit an item
  3082. * @param {Number} index
  3083. * @param {Object} itemData Object containing item properties:<br>
  3084. * {Date} start (required),
  3085. * {Date} end (optional),
  3086. * {String} content (required),
  3087. * {String} group (optional)
  3088. */
  3089. links.Timeline.prototype.changeItem = function (index, itemData) {
  3090. if (index >= this.items.length) {
  3091. throw "Cannot change item, index out of range";
  3092. }
  3093. var style = this.options.style;
  3094. var item = this.items[index];
  3095. // edit the item
  3096. if (itemData.start) {
  3097. item.start = itemData.start;
  3098. }
  3099. if (itemData.end) {
  3100. item.end = itemData.end;
  3101. }
  3102. if (itemData.content) {
  3103. item.content = itemData.content;
  3104. }
  3105. if (itemData.group) {
  3106. item.group = this.addGroup(itemData.group);
  3107. }
  3108. // update the original data table
  3109. this.updateData(index, itemData);
  3110. // redraw timeline
  3111. this.size.dataChanged = true;
  3112. this.redrawFrame();
  3113. this.recalcSize();
  3114. this.stackEvents(false);
  3115. this.redrawFrame();
  3116. this.size.dataChanged = false;
  3117. };
  3118. /**
  3119. * Find a group by its name.
  3120. * @param {String} group
  3121. * @return {Object} a group object or undefined when group is not found
  3122. */
  3123. links.Timeline.prototype.findGroup = function (group) {
  3124. var index = this.groupIndexes[group];
  3125. return (index != undefined) ? this.groups[index] : undefined;
  3126. };
  3127. /**
  3128. * Delete all groups
  3129. */
  3130. links.Timeline.prototype.deleteGroups = function () {
  3131. this.groups = [];
  3132. this.groupIndexes = {};
  3133. };
  3134. /**
  3135. * Add a group. When the group already exists, no new group is created
  3136. * but the existing group is returned.
  3137. * @param {String} groupName the name of the group
  3138. * @return {Object} groupObject
  3139. */
  3140. links.Timeline.prototype.addGroup = function (groupName) {
  3141. var groups = this.groups,
  3142. groupIndexes = this.groupIndexes,
  3143. groupObj = undefined;
  3144. var groupIndex = groupIndexes[groupName];
  3145. if (groupIndex === undefined && groupName !== undefined) {
  3146. groupObj = {
  3147. 'content': groupName,
  3148. 'labelTop': 0,
  3149. 'lineTop': 0
  3150. // note: this object will lateron get addition information,
  3151. // such as height and width of the group
  3152. };
  3153. groups.push(groupObj);
  3154. // sort the groups
  3155. groups = groups.sort(function (a, b) {
  3156. if (a.content > b.content) {
  3157. return 1;
  3158. }
  3159. if (a.content < b.content) {
  3160. return -1;
  3161. }
  3162. return 0;
  3163. });
  3164. // rebuilt the groupIndexes
  3165. for (var i = 0, iMax = groups.length; i < iMax; i++) {
  3166. groupIndexes[groups[i].content] = i;
  3167. }
  3168. }
  3169. else {
  3170. groupObj = groups[groupIndex];
  3171. }
  3172. return groupObj;
  3173. };
  3174. /**
  3175. * Cancel a change item
  3176. * This method can be called insed an event listener which catches the "change"
  3177. * event. The changed event position will be undone.
  3178. */
  3179. links.Timeline.prototype.cancelChange = function () {
  3180. this.applyChange = false;
  3181. };
  3182. /**
  3183. * Cancel deletion of an item
  3184. * This method can be called insed an event listener which catches the "delete"
  3185. * event. Deletion of the event will be undone.
  3186. */
  3187. links.Timeline.prototype.cancelDelete = function () {
  3188. this.applyDelete = false;
  3189. };
  3190. /**
  3191. * Cancel creation of a new item
  3192. * This method can be called insed an event listener which catches the "new"
  3193. * event. Creation of the new the event will be undone.
  3194. */
  3195. links.Timeline.prototype.cancelAdd = function () {
  3196. this.applyAdd = false;
  3197. };
  3198. /**
  3199. * Select an event. The visible chart range will be moved such that the selected
  3200. * event is placed in the middle.
  3201. * For example selection = [{row: 5}];
  3202. * @param {Array} selection An array with a column row, containing the row
  3203. * number (the id) of the event to be selected.
  3204. * @return {boolean} true if selection is succesfully set, else false.
  3205. */
  3206. links.Timeline.prototype.setSelection = function(selection) {
  3207. if (selection != undefined && selection.length > 0) {
  3208. if (selection[0].row != undefined) {
  3209. var index = selection[0].row;
  3210. if (this.items[index]) {
  3211. var item = this.items[index];
  3212. this.selectItem(index);
  3213. // move the visible chart range to the selected event.
  3214. var start = item.start;
  3215. var end = item.end;
  3216. var middle;
  3217. if (end != undefined) {
  3218. middle = new Date((end.valueOf() + start.valueOf()) / 2);
  3219. } else {
  3220. middle = new Date(start);
  3221. }
  3222. var diff = (this.end.valueOf() - this.start.valueOf()),
  3223. newStart = new Date(middle.valueOf() - diff/2),
  3224. newEnd = new Date(middle.valueOf() + diff/2);
  3225. this.setVisibleChartRange(newStart, newEnd);
  3226. return true;
  3227. }
  3228. }
  3229. }
  3230. else {
  3231. // unselect current selection
  3232. this.unselectItem();
  3233. }
  3234. return false;
  3235. };
  3236. /**
  3237. * Retrieve the currently selected event
  3238. * @return {Array} sel An array with a column row, containing the row number
  3239. * of the selected event. If there is no selection, an
  3240. * empty array is returned.
  3241. */
  3242. links.Timeline.prototype.getSelection = function() {
  3243. var sel = [];
  3244. if (this.selection) {
  3245. sel.push({"row": this.selection.index});
  3246. }
  3247. return sel;
  3248. };
  3249. /**
  3250. * Select an item by its index
  3251. * @param {Number} index
  3252. */
  3253. links.Timeline.prototype.selectItem = function(index) {
  3254. this.unselectItem();
  3255. this.selection = undefined;
  3256. if (this.items[index] !== undefined) {
  3257. var item = this.items[index],
  3258. domItem = item.dom;
  3259. this.selection = {
  3260. 'index': index,
  3261. 'item': domItem
  3262. };
  3263. if (this.options.editable) {
  3264. domItem.style.cursor = 'move';
  3265. }
  3266. switch (item.type) {
  3267. case 'range':
  3268. domItem.className = "timeline-event timeline-event-selected timeline-event-range";
  3269. break;
  3270. case 'box':
  3271. domItem.className = "timeline-event timeline-event-selected timeline-event-box";
  3272. domItem.line.className = "timeline-event timeline-event-selected timeline-event-line";
  3273. domItem.dot.className = "timeline-event timeline-event-selected timeline-event-dot";
  3274. break;
  3275. case 'dot':
  3276. domItem.className = "timeline-event timeline-event-selected";
  3277. domItem.dot.className = "timeline-event timeline-event-selected timeline-event-dot";
  3278. break;
  3279. }
  3280. /* TODO: cleanup this cannot work as this breaks any javscript action inside the item
  3281. // move the item to the end, such that it will be displayed on top of the other items
  3282. var parent = domItem.parentNode;
  3283. if (parent) {
  3284. parent.removeChild(domItem);
  3285. parent.appendChild(domItem);
  3286. }
  3287. */
  3288. }
  3289. };
  3290. /**
  3291. * Check if an item is currently selected
  3292. * @param {Number} index
  3293. * @return {boolean} true if row is selected, else false
  3294. */
  3295. links.Timeline.prototype.isSelected = function (index) {
  3296. return (this.selection && this.selection.index === index);
  3297. };
  3298. /**
  3299. * Unselect the currently selected event (if any)
  3300. */
  3301. links.Timeline.prototype.unselectItem = function() {
  3302. if (this.selection) {
  3303. var item = this.items[this.selection.index];
  3304. if (item && item.dom) {
  3305. var domItem = item.dom;
  3306. domItem.style.cursor = '';
  3307. switch (item.type) {
  3308. case 'range':
  3309. domItem.className = "timeline-event timeline-event-range";
  3310. break;
  3311. case 'box':
  3312. domItem.className = "timeline-event timeline-event-box";
  3313. domItem.line.className = "timeline-event timeline-event-line";
  3314. domItem.dot.className = "timeline-event timeline-event-dot";
  3315. break;
  3316. case 'dot':
  3317. domItem.className = "";
  3318. domItem.dot.className = "timeline-event timeline-event-dot";
  3319. break;
  3320. }
  3321. }
  3322. }
  3323. this.selection = undefined;
  3324. };
  3325. /**
  3326. * Stack the items such that they don't overlap. The items will have a minimal
  3327. * distance equal to options.eventMargin.
  3328. * @param {boolean} animate if animate is true, the items are moved to
  3329. * their new position animated
  3330. */
  3331. links.Timeline.prototype.stackEvents = function(animate) {
  3332. if (this.groups.length > 0) {
  3333. // under this conditions we refuse to stack the events
  3334. return;
  3335. }
  3336. if (animate == undefined) {
  3337. animate = false;
  3338. }
  3339. var sortedItems = this.stackOrder(this.items);
  3340. var finalItems = this.stackCalculateFinal(sortedItems, animate);
  3341. if (animate) {
  3342. // move animated to the final positions
  3343. var animation = this.animation;
  3344. if (!animation) {
  3345. animation = {};
  3346. this.animation = animation;
  3347. }
  3348. animation.finalItems = finalItems;
  3349. var timeline = this;
  3350. var step = function () {
  3351. var arrived = timeline.stackMoveOneStep(sortedItems, animation.finalItems);
  3352. timeline.recalcSize();
  3353. timeline.redrawFrame();
  3354. if (!arrived) {
  3355. animation.timer = setTimeout(step, 30);
  3356. }
  3357. else {
  3358. delete animation.finalItems;
  3359. delete animation.timer;
  3360. }
  3361. };
  3362. if (!animation.timer) {
  3363. animation.timer = setTimeout(step, 30);
  3364. }
  3365. }
  3366. else {
  3367. this.stackMoveToFinal(sortedItems, finalItems);
  3368. this.recalcSize();
  3369. }
  3370. };
  3371. /**
  3372. * Order the items in the array this.items. The order is determined via:
  3373. * - Ranges go before boxes and dots.
  3374. * - The item with the left most location goes first
  3375. * @param {Array} items Array with items
  3376. * @return {Array} sortedItems Array with sorted items
  3377. */
  3378. links.Timeline.prototype.stackOrder = function(items) {
  3379. // TODO: store the sorted items, to have less work later on
  3380. var sortedItems = items.concat([]);
  3381. var f = function (a, b) {
  3382. if (a.type == 'range' && b.type != 'range') {
  3383. return -1;
  3384. }
  3385. if (a.type != 'range' && b.type == 'range') {
  3386. return 1;
  3387. }
  3388. return (a.left - b.left);
  3389. };
  3390. sortedItems.sort(f);
  3391. return sortedItems;
  3392. };
  3393. /**
  3394. * Adjust vertical positions of the events such that they don't overlap each
  3395. * other.
  3396. */
  3397. links.Timeline.prototype.stackCalculateFinal = function(items) {
  3398. var size = this.size,
  3399. axisTop = size.axis.top,
  3400. options = this.options,
  3401. axisOnTop = options.axisOnTop,
  3402. eventMargin = options.eventMargin,
  3403. eventMarginAxis = options.eventMarginAxis,
  3404. finalItems = [];
  3405. // initialize final positions
  3406. for (var i = 0, iMax = items.length; i < iMax; i++) {
  3407. var item = items[i],
  3408. top,
  3409. left,
  3410. right,
  3411. bottom,
  3412. height = item.height,
  3413. width = item.width;
  3414. if (axisOnTop) {
  3415. top = axisTop + eventMarginAxis + eventMargin / 2;
  3416. }
  3417. else {
  3418. top = axisTop - height - eventMarginAxis - eventMargin / 2;
  3419. }
  3420. bottom = top + height;
  3421. switch (item.type) {
  3422. case 'range':
  3423. case 'dot':
  3424. left = this.timeToScreen(item.start);
  3425. right = item.end ? this.timeToScreen(item.end) : left + width;
  3426. break;
  3427. case 'box':
  3428. left = this.timeToScreen(item.start) - width / 2;
  3429. right = left + width;
  3430. break;
  3431. }
  3432. finalItems[i] = {
  3433. 'left': left,
  3434. 'top': top,
  3435. 'right': right,
  3436. 'bottom': bottom,
  3437. 'height': height,
  3438. 'item': item
  3439. };
  3440. }
  3441. if (this.options.stackEvents) {
  3442. // calculate new, non-overlapping positions
  3443. //var items = sortedItems;
  3444. for (var i = 0, iMax = finalItems.length; i < iMax; i++) {
  3445. //for (var i = finalItems.length - 1; i >= 0; i--) {
  3446. var finalItem = finalItems[i];
  3447. var collidingItem = null;
  3448. do {
  3449. // TODO: optimize checking for overlap. when there is a gap without items,
  3450. // you only need to check for items from the next item on, not from zero
  3451. collidingItem = this.stackEventsCheckOverlap(finalItems, i, 0, i-1);
  3452. if (collidingItem != null) {
  3453. // There is a collision. Reposition the event above the colliding element
  3454. if (axisOnTop) {
  3455. finalItem.top = collidingItem.top + collidingItem.height + eventMargin;
  3456. }
  3457. else {
  3458. finalItem.top = collidingItem.top - finalItem.height - eventMargin;
  3459. }
  3460. finalItem.bottom = finalItem.top + finalItem.height;
  3461. }
  3462. } while (collidingItem);
  3463. }
  3464. }
  3465. return finalItems;
  3466. };
  3467. /**
  3468. * Move the events one step in the direction of their final positions
  3469. * @param {Array} currentItems Array with the real items and their current
  3470. * positions
  3471. * @param {Array} finalItems Array with objects containing the final
  3472. * positions of the items
  3473. * @return {boolean} arrived True if all items have reached their final
  3474. * location, else false
  3475. */
  3476. links.Timeline.prototype.stackMoveOneStep = function(currentItems, finalItems) {
  3477. var arrived = true;
  3478. // apply new positions animated
  3479. for (i = 0, iMax = currentItems.length; i < iMax; i++) {
  3480. var finalItem = finalItems[i],
  3481. item = finalItem.item;
  3482. var topNow = parseInt(item.top);
  3483. var topFinal = parseInt(finalItem.top);
  3484. var diff = (topFinal - topNow);
  3485. if (diff) {
  3486. var step = (topFinal == topNow) ? 0 : ((topFinal > topNow) ? 1 : -1);
  3487. if (Math.abs(diff) > 4) step = diff / 4;
  3488. var topNew = parseInt(topNow + step);
  3489. if (topNew != topFinal) {
  3490. arrived = false;
  3491. }
  3492. item.top = topNew;
  3493. item.bottom = item.top + item.height;
  3494. }
  3495. else {
  3496. item.top = finalItem.top;
  3497. item.bottom = finalItem.bottom;
  3498. }
  3499. item.left = finalItem.left;
  3500. item.right = finalItem.right;
  3501. }
  3502. return arrived;
  3503. };
  3504. /**
  3505. * Move the events from their current position to the final position
  3506. * @param {Array} currentItems Array with the real items and their current
  3507. * positions
  3508. * @param {Array} finalItems Array with objects containing the final
  3509. * positions of the items
  3510. */
  3511. links.Timeline.prototype.stackMoveToFinal = function(currentItems, finalItems) {
  3512. // Put the events directly at there final position
  3513. for (i = 0, iMax = currentItems.length; i < iMax; i++) {
  3514. var current = currentItems[i],
  3515. finalItem = finalItems[i];
  3516. current.left = finalItem.left;
  3517. current.top = finalItem.top;
  3518. current.right = finalItem.right;
  3519. current.bottom = finalItem.bottom;
  3520. }
  3521. };
  3522. /**
  3523. * Check if the destiny position of given item overlaps with any
  3524. * of the other items from index itemStart to itemEnd.
  3525. * @param {Array} items Array with items
  3526. * @param {int} itemIndex Number of the item to be checked for overlap
  3527. * @param {int} itemStart First item to be checked.
  3528. * @param {int} itemEnd Last item to be checked.
  3529. * @return {Object} colliding item, or undefined when no collisions
  3530. */
  3531. links.Timeline.prototype.stackEventsCheckOverlap = function(items, itemIndex,
  3532. itemStart, itemEnd) {
  3533. var eventMargin = this.options.eventMargin,
  3534. collision = this.collision;
  3535. // we loop from end to start, as we suppose that the chance of a
  3536. // collision is larger for items at the end, so check these first.
  3537. var item1 = items[itemIndex];
  3538. for (var i = itemEnd; i >= itemStart; i--) {
  3539. var item2 = items[i];
  3540. if (collision(item1, item2, eventMargin)) {
  3541. if (i != itemIndex) {
  3542. return item2;
  3543. }
  3544. }
  3545. }
  3546. return undefined;
  3547. };
  3548. /**
  3549. * Test if the two provided items collide
  3550. * The items must have parameters left, right, top, and bottom.
  3551. * @param {Element} item1 The first item
  3552. * @param {Element} item2 The second item
  3553. * @param {Number} margin A minimum required margin. Optional.
  3554. * If margin is provided, the two items will be
  3555. * marked colliding when they overlap or
  3556. * when the margin between the two is smaller than
  3557. * the requested margin.
  3558. * @return {boolean} true if item1 and item2 collide, else false
  3559. */
  3560. links.Timeline.prototype.collision = function(item1, item2, margin) {
  3561. // set margin if not specified
  3562. if (margin == undefined) {
  3563. margin = 0;
  3564. }
  3565. // calculate if there is overlap (collision)
  3566. return (item1.left - margin < item2.right &&
  3567. item1.right + margin > item2.left &&
  3568. item1.top - margin < item2.bottom &&
  3569. item1.bottom + margin > item2.top);
  3570. };
  3571. /**
  3572. * fire an event
  3573. * @param {String} event The name of an event, for example "rangechange" or "edit"
  3574. */
  3575. links.Timeline.prototype.trigger = function (event) {
  3576. // built up properties
  3577. var properties = null;
  3578. switch (event) {
  3579. case 'rangechange':
  3580. case 'rangechanged':
  3581. properties = {
  3582. 'start': new Date(this.start),
  3583. 'end': new Date(this.end)
  3584. };
  3585. break;
  3586. case 'timechange':
  3587. case 'timechanged':
  3588. properties = {
  3589. 'time': new Date(this.customTime)
  3590. };
  3591. break;
  3592. }
  3593. // trigger the links event bus
  3594. links.events.trigger(this, event, properties);
  3595. // trigger the google event bus
  3596. if (google && google.visualization) {
  3597. google.visualization.events.trigger(this, event, properties);
  3598. }
  3599. };
  3600. /** ------------------------------------------------------------------------ **/
  3601. /**
  3602. * Event listener (singleton)
  3603. */
  3604. links.events = links.events || {
  3605. 'listeners': [],
  3606. /**
  3607. * Find a single listener by its object
  3608. * @param {Object} object
  3609. * @return {Number} index -1 when not found
  3610. */
  3611. 'indexOf': function (object) {
  3612. var listeners = this.listeners;
  3613. for (var i = 0, iMax = this.listeners.length; i < iMax; i++) {
  3614. var listener = listeners[i];
  3615. if (listener && listener.object == object) {
  3616. return i;
  3617. }
  3618. }
  3619. return -1;
  3620. },
  3621. /**
  3622. * Add an event listener
  3623. * @param {Object} object
  3624. * @param {String} event The name of an event, for example 'select'
  3625. * @param {function} callback The callback method, called when the
  3626. * event takes place
  3627. */
  3628. 'addListener': function (object, event, callback) {
  3629. var index = this.indexOf(object);
  3630. var listener = this.listeners[index];
  3631. if (!listener) {
  3632. listener = {
  3633. 'object': object,
  3634. 'events': {}
  3635. };
  3636. this.listeners.push(listener);
  3637. }
  3638. var callbacks = listener.events[event];
  3639. if (!callbacks) {
  3640. callbacks = [];
  3641. listener.events[event] = callbacks;
  3642. }
  3643. // add the callback if it does not yet exist
  3644. if (callbacks.indexOf(callback) == -1) {
  3645. callbacks.push(callback);
  3646. }
  3647. },
  3648. /**
  3649. * Remove an event listener
  3650. * @param {Object} object
  3651. * @param {String} event The name of an event, for example 'select'
  3652. * @param {function} callback The registered callback method
  3653. */
  3654. 'removeListener': function (object, event, callback) {
  3655. var index = this.indexOf(object);
  3656. var listener = this.listeners[index];
  3657. if (listener) {
  3658. var callbacks = listener.events[event];
  3659. if (callbacks) {
  3660. var index = callbacks.indexOf(callback);
  3661. if (index != -1) {
  3662. callbacks.splice(index, 1);
  3663. }
  3664. // remove the array when empty
  3665. if (callbacks.length == 0) {
  3666. delete listener.events[event];
  3667. }
  3668. }
  3669. // count the number of registered events. remove listener when empty
  3670. var count = 0;
  3671. var events = listener.events;
  3672. for (var e in events) {
  3673. if (events.hasOwnProperty(e)) {
  3674. count++;
  3675. }
  3676. }
  3677. if (count == 0) {
  3678. delete this.listeners[index];
  3679. }
  3680. }
  3681. },
  3682. /**
  3683. * Remove all registered event listeners
  3684. */
  3685. 'removeAllListeners': function () {
  3686. this.listeners = [];
  3687. },
  3688. /**
  3689. * Trigger an event. All registered event handlers will be called
  3690. * @param {Object} object
  3691. * @param {String} event
  3692. * @param {Object} properties (optional)
  3693. */
  3694. 'trigger': function (object, event, properties) {
  3695. var index = this.indexOf(object);
  3696. var listener = this.listeners[index];
  3697. if (listener) {
  3698. var callbacks = listener.events[event];
  3699. if (callbacks) {
  3700. for (var i = 0, iMax = callbacks.length; i < iMax; i++) {
  3701. callbacks[i](properties);
  3702. }
  3703. }
  3704. }
  3705. }
  3706. };
  3707. /** ------------------------------------------------------------------------ **/
  3708. /**
  3709. * @constructor links.Timeline.StepDate
  3710. * The class StepDate is an iterator for dates. You provide a start date and an
  3711. * end date. The class itself determines the best scale (step size) based on the
  3712. * provided start Date, end Date, and minimumStep.
  3713. *
  3714. * If minimumStep is provided, the step size is chosen as close as possible
  3715. * to the minimumStep but larger than minimumStep. If minimumStep is not
  3716. * provided, the scale is set to 1 DAY.
  3717. * The minimumStep should correspond with the onscreen size of about 6 characters
  3718. *
  3719. * Alternatively, you can set a scale by hand.
  3720. * After creation, you can initialize the class by executing start(). Then you
  3721. * can iterate from the start date to the end date via next(). You can check if
  3722. * the end date is reached with the function end(). After each step, you can
  3723. * retrieve the current date via get().
  3724. * The class step has scales ranging from milliseconds, seconds, minutes, hours,
  3725. * days, to years.
  3726. *
  3727. * Version: 1.0
  3728. *
  3729. * @param {Date} start The start date, for example new Date(2010, 9, 21)
  3730. * or new Date(2010, 9,21,23,45,00)
  3731. * @param {Date} end The end date
  3732. * @param {int} minimumStep Optional. Minimum step size in milliseconds
  3733. */
  3734. links.Timeline.StepDate = function(start, end, minimumStep) {
  3735. // variables
  3736. this.current = new Date();
  3737. this._start = new Date();
  3738. this._end = new Date();
  3739. this.autoScale = true;
  3740. this.scale = links.Timeline.StepDate.SCALE.DAY;
  3741. this.step = 1;
  3742. // initialize the range
  3743. this.setRange(start, end, minimumStep);
  3744. };
  3745. /// enum scale
  3746. links.Timeline.StepDate.SCALE = { MILLISECOND : 1,
  3747. SECOND : 2,
  3748. MINUTE : 3,
  3749. HOUR : 4,
  3750. DAY : 5,
  3751. MONTH : 6,
  3752. YEAR : 7};
  3753. /**
  3754. * Set a new range
  3755. * If minimumStep is provided, the step size is chosen as close as possible
  3756. * to the minimumStep but larger than minimumStep. If minimumStep is not
  3757. * provided, the scale is set to 1 DAY.
  3758. * The minimumStep should correspond with the onscreen size of about 6 characters
  3759. * @param {Date} start The start date and time.
  3760. * @param {Date} end The end date and time.
  3761. * @param {int} minimumStep Optional. Minimum step size in milliseconds
  3762. */
  3763. links.Timeline.StepDate.prototype.setRange = function(start, end, minimumStep) {
  3764. if (isNaN(start) || isNaN(end)) {
  3765. //throw "No legal start or end date in method setRange";
  3766. return;
  3767. }
  3768. this._start = (start != undefined) ? new Date(start) : new Date();
  3769. this._end = (end != undefined) ? new Date(end) : new Date();
  3770. if (this.autoScale) {
  3771. this.setMinimumStep(minimumStep);
  3772. }
  3773. };
  3774. /**
  3775. * Set the step iterator to the start date.
  3776. */
  3777. links.Timeline.StepDate.prototype.start = function() {
  3778. this.current = new Date(this._start);
  3779. this.roundToMinor();
  3780. };
  3781. /**
  3782. * Round the current date to the first minor date value
  3783. * This must be executed once when the current date is set to start Date
  3784. */
  3785. links.Timeline.StepDate.prototype.roundToMinor = function() {
  3786. // round to floor
  3787. // IMPORTANT: we have no breaks in this switch! (this is no bug)
  3788. switch (this.scale) {
  3789. case links.Timeline.StepDate.SCALE.YEAR:
  3790. this.current.setFullYear(this.step * Math.floor(this.current.getFullYear() / this.step));
  3791. this.current.setMonth(0);
  3792. case links.Timeline.StepDate.SCALE.MONTH: this.current.setDate(1);
  3793. case links.Timeline.StepDate.SCALE.DAY: this.current.setHours(0);
  3794. case links.Timeline.StepDate.SCALE.HOUR: this.current.setMinutes(0);
  3795. case links.Timeline.StepDate.SCALE.MINUTE: this.current.setSeconds(0);
  3796. case links.Timeline.StepDate.SCALE.SECOND: this.current.setMilliseconds(0);
  3797. //case links.Timeline.StepDate.SCALE.MILLISECOND: // nothing to do for milliseconds
  3798. }
  3799. if (this.step != 1) {
  3800. // round down to the first minor value that is a multiple of the current step size
  3801. switch (this.scale) {
  3802. case links.Timeline.StepDate.SCALE.MILLISECOND: this.current.setMilliseconds(this.current.getMilliseconds() - this.current.getMilliseconds() % this.step); break;
  3803. case links.Timeline.StepDate.SCALE.SECOND: this.current.setSeconds(this.current.getSeconds() - this.current.getSeconds() % this.step); break;
  3804. case links.Timeline.StepDate.SCALE.MINUTE: this.current.setMinutes(this.current.getMinutes() - this.current.getMinutes() % this.step); break;
  3805. case links.Timeline.StepDate.SCALE.HOUR: this.current.setHours(this.current.getHours() - this.current.getHours() % this.step); break;
  3806. case links.Timeline.StepDate.SCALE.DAY: this.current.setDate((this.current.getDate()-1) - (this.current.getDate()-1) % this.step + 1); break;
  3807. case links.Timeline.StepDate.SCALE.MONTH: this.current.setMonth(this.current.getMonth() - this.current.getMonth() % this.step); break;
  3808. case links.Timeline.StepDate.SCALE.YEAR: this.current.setFullYear(this.current.getFullYear() - this.current.getFullYear() % this.step); break;
  3809. default: break;
  3810. }
  3811. }
  3812. };
  3813. /**
  3814. * Check if the end date is reached
  3815. * @return {boolean} true if the current date has passed the end date
  3816. */
  3817. links.Timeline.StepDate.prototype.end = function () {
  3818. return (this.current.getTime() > this._end.getTime());
  3819. };
  3820. /**
  3821. * Do the next step
  3822. */
  3823. links.Timeline.StepDate.prototype.next = function() {
  3824. var prev = this.current.getTime();
  3825. // Two cases, needed to prevent issues with switching daylight savings
  3826. // (end of March and end of October)
  3827. if (this.current.getMonth() < 6) {
  3828. switch (this.scale)
  3829. {
  3830. case links.Timeline.StepDate.SCALE.MILLISECOND:
  3831. this.current = new Date(this.current.getTime() + this.step); break;
  3832. case links.Timeline.StepDate.SCALE.SECOND: this.current = new Date(this.current.getTime() + this.step * 1000); break;
  3833. case links.Timeline.StepDate.SCALE.MINUTE: this.current = new Date(this.current.getTime() + this.step * 1000 * 60); break;
  3834. case links.Timeline.StepDate.SCALE.HOUR:
  3835. this.current = new Date(this.current.getTime() + this.step * 1000 * 60 * 60);
  3836. // in case of skipping an hour for daylight savings, adjust the hour again (else you get: 0h 5h 9h ... instead of 0h 4h 8h ...)
  3837. var h = this.current.getHours();
  3838. this.current.setHours(h - (h % this.step));
  3839. break;
  3840. case links.Timeline.StepDate.SCALE.DAY: this.current.setDate(this.current.getDate() + this.step); break;
  3841. case links.Timeline.StepDate.SCALE.MONTH: this.current.setMonth(this.current.getMonth() + this.step); break;
  3842. case links.Timeline.StepDate.SCALE.YEAR: this.current.setFullYear(this.current.getFullYear() + this.step); break;
  3843. default: break;
  3844. }
  3845. }
  3846. else {
  3847. switch (this.scale)
  3848. {
  3849. case links.Timeline.StepDate.SCALE.MILLISECOND:
  3850. this.current = new Date(this.current.getTime() + this.step); break;
  3851. case links.Timeline.StepDate.SCALE.SECOND: this.current.setSeconds(this.current.getSeconds() + this.step); break;
  3852. case links.Timeline.StepDate.SCALE.MINUTE: this.current.setMinutes(this.current.getMinutes() + this.step); break;
  3853. case links.Timeline.StepDate.SCALE.HOUR: this.current.setHours(this.current.getHours() + this.step); break;
  3854. case links.Timeline.StepDate.SCALE.DAY: this.current.setDate(this.current.getDate() + this.step); break;
  3855. case links.Timeline.StepDate.SCALE.MONTH: this.current.setMonth(this.current.getMonth() + this.step); break;
  3856. case links.Timeline.StepDate.SCALE.YEAR: this.current.setFullYear(this.current.getFullYear() + this.step); break;
  3857. default: break;
  3858. }
  3859. }
  3860. if (this.step != 1) {
  3861. // round down to the correct major value
  3862. switch (this.scale) {
  3863. case links.Timeline.StepDate.SCALE.MILLISECOND: if(this.current.getMilliseconds() < this.step) this.current.setMilliseconds(0); break;
  3864. case links.Timeline.StepDate.SCALE.SECOND: if(this.current.getSeconds() < this.step) this.current.setSeconds(0); break;
  3865. case links.Timeline.StepDate.SCALE.MINUTE: if(this.current.getMinutes() < this.step) this.current.setMinutes(0); break;
  3866. case links.Timeline.StepDate.SCALE.HOUR: if(this.current.getHours() < this.step) this.current.setHours(0); break;
  3867. case links.Timeline.StepDate.SCALE.DAY: if(this.current.getDate() < this.step+1) this.current.setDate(1); break;
  3868. case links.Timeline.StepDate.SCALE.MONTH: if(this.current.getMonth() < this.step) this.current.setMonth(0); break;
  3869. case links.Timeline.StepDate.SCALE.YEAR: break; // nothing to do for year
  3870. default: break;
  3871. }
  3872. }
  3873. // safety mechanism: if current time is still unchanged, move to the end
  3874. if (this.current.getTime() == prev) {
  3875. this.current = new Date(this._end);
  3876. }
  3877. };
  3878. /**
  3879. * Get the current datetime
  3880. * @return {Date} current The current date
  3881. */
  3882. links.Timeline.StepDate.prototype.getCurrent = function() {
  3883. return this.current;
  3884. };
  3885. /**
  3886. * Set a custom scale. Autoscaling will be disabled.
  3887. * For example setScale(SCALE.MINUTES, 5) will result
  3888. * in minor steps of 5 minutes, and major steps of an hour.
  3889. *
  3890. * @param {links.Timeline.StepDate.SCALE} newScale
  3891. * A scale. Choose from SCALE.MILLISECOND,
  3892. * SCALE.SECOND, SCALE.MINUTE, SCALE.HOUR,
  3893. * SCALE.DAY, SCALE.MONTH, SCALE.YEAR.
  3894. * @param {int} newStep A step size, by default 1. Choose for
  3895. * example 1, 2, 5, or 10.
  3896. */
  3897. links.Timeline.StepDate.prototype.setScale = function(newScale, newStep) {
  3898. this.scale = newScale;
  3899. if (newStep > 0)
  3900. this.step = newStep;
  3901. this.autoScale = false;
  3902. };
  3903. /**
  3904. * Enable or disable autoscaling
  3905. * @param {boolean} enable If true, autoascaling is set true
  3906. */
  3907. links.Timeline.StepDate.prototype.setAutoScale = function (enable) {
  3908. this.autoScale = enable;
  3909. };
  3910. /**
  3911. * Automatically determine the scale that bests fits the provided minimum step
  3912. * @param {int} minimumStep The minimum step size in milliseconds
  3913. */
  3914. links.Timeline.StepDate.prototype.setMinimumStep = function(minimumStep) {
  3915. if (minimumStep == undefined)
  3916. return;
  3917. var stepYear = (1000 * 60 * 60 * 24 * 30 * 12);
  3918. var stepMonth = (1000 * 60 * 60 * 24 * 30);
  3919. var stepDay = (1000 * 60 * 60 * 24);
  3920. var stepHour = (1000 * 60 * 60);
  3921. var stepMinute = (1000 * 60);
  3922. var stepSecond = (1000);
  3923. var stepMillisecond= (1);
  3924. // find the smallest step that is larger than the provided minimumStep
  3925. if (stepYear*1000 > minimumStep) {this.scale = links.Timeline.StepDate.SCALE.YEAR; this.step = 1000;}
  3926. if (stepYear*500 > minimumStep) {this.scale = links.Timeline.StepDate.SCALE.YEAR; this.step = 500;}
  3927. if (stepYear*100 > minimumStep) {this.scale = links.Timeline.StepDate.SCALE.YEAR; this.step = 100;}
  3928. if (stepYear*50 > minimumStep) {this.scale = links.Timeline.StepDate.SCALE.YEAR; this.step = 50;}
  3929. if (stepYear*10 > minimumStep) {this.scale = links.Timeline.StepDate.SCALE.YEAR; this.step = 10;}
  3930. if (stepYear*5 > minimumStep) {this.scale = links.Timeline.StepDate.SCALE.YEAR; this.step = 5;}
  3931. if (stepYear > minimumStep) {this.scale = links.Timeline.StepDate.SCALE.YEAR; this.step = 1;}
  3932. if (stepMonth*3 > minimumStep) {this.scale = links.Timeline.StepDate.SCALE.MONTH; this.step = 3;}
  3933. if (stepMonth > minimumStep) {this.scale = links.Timeline.StepDate.SCALE.MONTH; this.step = 1;}
  3934. if (stepDay*5 > minimumStep) {this.scale = links.Timeline.StepDate.SCALE.DAY; this.step = 5;}
  3935. if (stepDay*2 > minimumStep) {this.scale = links.Timeline.StepDate.SCALE.DAY; this.step = 2;}
  3936. if (stepDay > minimumStep) {this.scale = links.Timeline.StepDate.SCALE.DAY; this.step = 1;}
  3937. if (stepHour*4 > minimumStep) {this.scale = links.Timeline.StepDate.SCALE.HOUR; this.step = 4;}
  3938. if (stepHour > minimumStep) {this.scale = links.Timeline.StepDate.SCALE.HOUR; this.step = 1;}
  3939. if (stepMinute*15 > minimumStep) {this.scale = links.Timeline.StepDate.SCALE.MINUTE; this.step = 15;}
  3940. if (stepMinute*10 > minimumStep) {this.scale = links.Timeline.StepDate.SCALE.MINUTE; this.step = 10;}
  3941. if (stepMinute*5 > minimumStep) {this.scale = links.Timeline.StepDate.SCALE.MINUTE; this.step = 5;}
  3942. if (stepMinute > minimumStep) {this.scale = links.Timeline.StepDate.SCALE.MINUTE; this.step = 1;}
  3943. if (stepSecond*15 > minimumStep) {this.scale = links.Timeline.StepDate.SCALE.SECOND; this.step = 15;}
  3944. if (stepSecond*10 > minimumStep) {this.scale = links.Timeline.StepDate.SCALE.SECOND; this.step = 10;}
  3945. if (stepSecond*5 > minimumStep) {this.scale = links.Timeline.StepDate.SCALE.SECOND; this.step = 5;}
  3946. if (stepSecond > minimumStep) {this.scale = links.Timeline.StepDate.SCALE.SECOND; this.step = 1;}
  3947. if (stepMillisecond*200 > minimumStep) {this.scale = links.Timeline.StepDate.SCALE.MILLISECOND; this.step = 200;}
  3948. if (stepMillisecond*100 > minimumStep) {this.scale = links.Timeline.StepDate.SCALE.MILLISECOND; this.step = 100;}
  3949. if (stepMillisecond*50 > minimumStep) {this.scale = links.Timeline.StepDate.SCALE.MILLISECOND; this.step = 50;}
  3950. if (stepMillisecond*10 > minimumStep) {this.scale = links.Timeline.StepDate.SCALE.MILLISECOND; this.step = 10;}
  3951. if (stepMillisecond*5 > minimumStep) {this.scale = links.Timeline.StepDate.SCALE.MILLISECOND; this.step = 5;}
  3952. if (stepMillisecond > minimumStep) {this.scale = links.Timeline.StepDate.SCALE.MILLISECOND; this.step = 1;}
  3953. };
  3954. /**
  3955. * Snap a date to a rounded value. The snap intervals are dependent on the
  3956. * current scale and step.
  3957. * @param {Date} date the date to be snapped
  3958. */
  3959. links.Timeline.StepDate.prototype.snap = function(date) {
  3960. if (this.scale == links.Timeline.StepDate.SCALE.YEAR) {
  3961. var year = date.getFullYear() + Math.round(date.getMonth() / 12);
  3962. date.setFullYear(Math.round(year / this.step) * this.step);
  3963. date.setMonth(0);
  3964. date.setDate(0);
  3965. date.setHours(0);
  3966. date.setMinutes(0);
  3967. date.setSeconds(0);
  3968. date.setMilliseconds(0);
  3969. }
  3970. else if (this.scale == links.Timeline.StepDate.SCALE.MONTH) {
  3971. if (date.getDate() > 15) {
  3972. date.setDate(1);
  3973. date.setMonth(date.getMonth() + 1);
  3974. // important: first set Date to 1, after that change the month.
  3975. }
  3976. else {
  3977. date.setDate(1);
  3978. }
  3979. date.setHours(0);
  3980. date.setMinutes(0);
  3981. date.setSeconds(0);
  3982. date.setMilliseconds(0);
  3983. }
  3984. else if (this.scale == links.Timeline.StepDate.SCALE.DAY) {
  3985. switch (this.step) {
  3986. case 5:
  3987. case 2:
  3988. date.setHours(Math.round(date.getHours() / 24) * 24); break;
  3989. default:
  3990. date.setHours(Math.round(date.getHours() / 12) * 12); break;
  3991. }
  3992. date.setMinutes(0);
  3993. date.setSeconds(0);
  3994. date.setMilliseconds(0);
  3995. }
  3996. else if (this.scale == links.Timeline.StepDate.SCALE.HOUR) {
  3997. switch (this.step) {
  3998. case 4:
  3999. date.setMinutes(Math.round(date.getMinutes() / 60) * 60); break;
  4000. default:
  4001. date.setMinutes(Math.round(date.getMinutes() / 30) * 30); break;
  4002. }
  4003. date.setSeconds(0);
  4004. date.setMilliseconds(0);
  4005. } else if (this.scale == links.Timeline.StepDate.SCALE.MINUTE) {
  4006. switch (this.step) {
  4007. case 15:
  4008. case 10:
  4009. date.setMinutes(Math.round(date.getMinutes() / 5) * 5);
  4010. date.setSeconds(0);
  4011. break;
  4012. case 5:
  4013. date.setSeconds(Math.round(date.getSeconds() / 60) * 60); break;
  4014. default:
  4015. date.setSeconds(Math.round(date.getSeconds() / 30) * 30); break;
  4016. }
  4017. date.setMilliseconds(0);
  4018. }
  4019. else if (this.scale == links.Timeline.StepDate.SCALE.SECOND) {
  4020. switch (this.step) {
  4021. case 15:
  4022. case 10:
  4023. date.setSeconds(Math.round(date.getSeconds() / 5) * 5);
  4024. date.setMilliseconds(0);
  4025. break;
  4026. case 5:
  4027. date.setMilliseconds(Math.round(date.getMilliseconds() / 1000) * 1000); break;
  4028. default:
  4029. date.setMilliseconds(Math.round(date.getMilliseconds() / 500) * 500); break;
  4030. }
  4031. }
  4032. else if (this.scale == links.Timeline.StepDate.SCALE.MILLISECOND) {
  4033. var step = this.step > 5 ? this.step / 2 : 1;
  4034. date.setMilliseconds(Math.round(date.getMilliseconds() / step) * step);
  4035. }
  4036. };
  4037. /**
  4038. * Check if the current step is a major step (for example when the step
  4039. * is DAY, a major step is each first day of the MONTH)
  4040. * @return true if current date is major, else false.
  4041. */
  4042. links.Timeline.StepDate.prototype.isMajor = function() {
  4043. switch (this.scale)
  4044. {
  4045. case links.Timeline.StepDate.SCALE.MILLISECOND:
  4046. return (this.current.getMilliseconds() == 0);
  4047. case links.Timeline.StepDate.SCALE.SECOND:
  4048. return (this.current.getSeconds() == 0);
  4049. case links.Timeline.StepDate.SCALE.MINUTE:
  4050. return (this.current.getHours() == 0) && (this.current.getMinutes() == 0);
  4051. // Note: this is no bug. Major label is equal for both minute and hour scale
  4052. case links.Timeline.StepDate.SCALE.HOUR:
  4053. return (this.current.getHours() == 0);
  4054. case links.Timeline.StepDate.SCALE.DAY:
  4055. return (this.current.getDate() == 1);
  4056. case links.Timeline.StepDate.SCALE.MONTH:
  4057. return (this.current.getMonth() == 0);
  4058. case links.Timeline.StepDate.SCALE.YEAR:
  4059. return false;
  4060. default:
  4061. return false;
  4062. }
  4063. };
  4064. /**
  4065. * Returns formatted text for the minor axislabel, depending on the current
  4066. * date and the scale. For example when scale is MINUTE, the current time is
  4067. * formatted as "hh:mm".
  4068. * @param {Date} [date] custom date. if not provided, current date is taken
  4069. * @return {string} minor axislabel
  4070. */
  4071. links.Timeline.StepDate.prototype.getLabelMinor = function(date) {
  4072. var MONTHS_SHORT = new Array("Jan", "Feb", "Mar",
  4073. "Apr", "May", "Jun",
  4074. "Jul", "Aug", "Sep",
  4075. "Oct", "Nov", "Dec");
  4076. if (date == undefined) {
  4077. date = this.current;
  4078. }
  4079. switch (this.scale)
  4080. {
  4081. case links.Timeline.StepDate.SCALE.MILLISECOND: return String(date.getMilliseconds());
  4082. case links.Timeline.StepDate.SCALE.SECOND: return String(date.getSeconds());
  4083. case links.Timeline.StepDate.SCALE.MINUTE: return this.addZeros(date.getHours(), 2) + ":" +
  4084. this.addZeros(date.getMinutes(), 2);
  4085. case links.Timeline.StepDate.SCALE.HOUR: return this.addZeros(date.getHours(), 2) + ":" +
  4086. this.addZeros(date.getMinutes(), 2);
  4087. case links.Timeline.StepDate.SCALE.DAY: return String(date.getDate());
  4088. case links.Timeline.StepDate.SCALE.MONTH: return MONTHS_SHORT[date.getMonth()]; // month is zero based
  4089. case links.Timeline.StepDate.SCALE.YEAR: return String(date.getFullYear());
  4090. default: return "";
  4091. }
  4092. };
  4093. /**
  4094. * Returns formatted text for the major axislabel, depending on the current
  4095. * date and the scale. For example when scale is MINUTE, the major scale is
  4096. * hours, and the hour will be formatted as "hh".
  4097. * @param {Date} [date] custom date. if not provided, current date is taken
  4098. * @return {string} major axislabel
  4099. */
  4100. links.Timeline.StepDate.prototype.getLabelMajor = function(date) {
  4101. var MONTHS = new Array("January", "February", "March",
  4102. "April", "May", "June",
  4103. "July", "August", "September",
  4104. "October", "November", "December");
  4105. var DAYS = new Array("Sunday", "Monday", "Tuesday",
  4106. "Wednesday", "Thursday", "Friday", "Saturday");
  4107. if (date == undefined) {
  4108. date = this.current;
  4109. }
  4110. switch (this.scale) {
  4111. case links.Timeline.StepDate.SCALE.MILLISECOND:
  4112. return this.addZeros(date.getHours(), 2) + ":" +
  4113. this.addZeros(date.getMinutes(), 2) + ":" +
  4114. this.addZeros(date.getSeconds(), 2);
  4115. case links.Timeline.StepDate.SCALE.SECOND:
  4116. return date.getDate() + " " +
  4117. MONTHS[date.getMonth()] + " " +
  4118. this.addZeros(date.getHours(), 2) + ":" +
  4119. this.addZeros(date.getMinutes(), 2);
  4120. case links.Timeline.StepDate.SCALE.MINUTE:
  4121. return DAYS[date.getDay()] + " " +
  4122. date.getDate() + " " +
  4123. MONTHS[date.getMonth()] + " " +
  4124. date.getFullYear();
  4125. case links.Timeline.StepDate.SCALE.HOUR:
  4126. return DAYS[date.getDay()] + " " +
  4127. date.getDate() + " " +
  4128. MONTHS[date.getMonth()] + " " +
  4129. date.getFullYear();
  4130. case links.Timeline.StepDate.SCALE.DAY:
  4131. return MONTHS[date.getMonth()] + " " +
  4132. date.getFullYear();
  4133. case links.Timeline.StepDate.SCALE.MONTH:
  4134. return String(date.getFullYear());
  4135. default:
  4136. return "";
  4137. }
  4138. };
  4139. /**
  4140. * Add leading zeros to the given value to match the desired length.
  4141. * For example addZeros(123, 5) returns "00123"
  4142. * @param {int} value A value
  4143. * @param {int} len Desired final length
  4144. * @return {string} value with leading zeros
  4145. */
  4146. links.Timeline.StepDate.prototype.addZeros = function(value, len) {
  4147. var str = "" + value;
  4148. while (str.length < len) {
  4149. str = "0" + str;
  4150. }
  4151. return str;
  4152. };
  4153. /** ------------------------------------------------------------------------ **/
  4154. /**
  4155. * Image Loader service.
  4156. * can be used to get a callback when a certain image is loaded
  4157. *
  4158. */
  4159. links.imageloader = (function () {
  4160. var urls = {}; // the loaded urls
  4161. var callbacks = {}; // the urls currently being loaded. Each key contains
  4162. // an array with callbacks
  4163. /**
  4164. * Check if an image url is loaded
  4165. * @param {String} url
  4166. * @return {boolean} loaded True when loaded, false when not loaded
  4167. * or when being loaded
  4168. */
  4169. function isLoaded (url) {
  4170. if (urls[url] == true) {
  4171. return true;
  4172. }
  4173. var image = new Image();
  4174. image.src = url;
  4175. if (image.complete) {
  4176. return true;
  4177. }
  4178. return false;
  4179. }
  4180. /**
  4181. * Check if an image url is being loaded
  4182. * @param {String} url
  4183. * @return {boolean} loading True when being loaded, false when not loading
  4184. * or when already loaded
  4185. */
  4186. function isLoading (url) {
  4187. return (callbacks[url] != undefined);
  4188. }
  4189. /**
  4190. * Load given image url
  4191. * @param {String} url
  4192. * @param {function} callback
  4193. * @param {boolean} sendCallbackWhenAlreadyLoaded optional
  4194. */
  4195. function load (url, callback, sendCallbackWhenAlreadyLoaded) {
  4196. if (sendCallbackWhenAlreadyLoaded == undefined) {
  4197. sendCallbackWhenAlreadyLoaded = true;
  4198. }
  4199. if (isLoaded(url)) {
  4200. if (sendCallbackWhenAlreadyLoaded) {
  4201. callback(url);
  4202. }
  4203. return;
  4204. }
  4205. if (isLoading(url) && !sendCallbackWhenAlreadyLoaded) {
  4206. return;
  4207. }
  4208. var c = callbacks[url];
  4209. if (!c) {
  4210. var image = new Image();
  4211. image.src = url;
  4212. c = [];
  4213. callbacks[url] = c;
  4214. image.onload = function (event) {
  4215. urls[url] = true;
  4216. delete callbacks[url];
  4217. for (var i = 0; i < c.length; i++) {
  4218. c[i](url);
  4219. }
  4220. }
  4221. }
  4222. if (c.indexOf(callback) == -1) {
  4223. c.push(callback);
  4224. }
  4225. }
  4226. return {
  4227. 'isLoaded': isLoaded,
  4228. 'isLoading': isLoading,
  4229. 'load': load
  4230. };
  4231. })();
  4232. /** ------------------------------------------------------------------------ **/
  4233. /**
  4234. * Add and event listener. Works for all browsers
  4235. * @param {Element} element An html element
  4236. * @param {string} action The action, for example "click",
  4237. * without the prefix "on"
  4238. * @param {function} listener The callback function to be executed
  4239. * @param {boolean} useCapture
  4240. */
  4241. links.Timeline.addEventListener = function (element, action, listener, useCapture) {
  4242. if (element.addEventListener) {
  4243. if (useCapture === undefined)
  4244. useCapture = false;
  4245. if (action === "mousewheel" && navigator.userAgent.indexOf("Firefox") >= 0) {
  4246. action = "DOMMouseScroll"; // For Firefox
  4247. }
  4248. element.addEventListener(action, listener, useCapture);
  4249. } else {
  4250. element.attachEvent("on" + action, listener); // IE browsers
  4251. }
  4252. };
  4253. /**
  4254. * Remove an event listener from an element
  4255. * @param {Element} element An html dom element
  4256. * @param {string} action The name of the event, for example "mousedown"
  4257. * @param {function} listener The listener function
  4258. * @param {boolean} useCapture
  4259. */
  4260. links.Timeline.removeEventListener = function(element, action, listener, useCapture) {
  4261. if (element.removeEventListener) {
  4262. // non-IE browsers
  4263. if (useCapture === undefined)
  4264. useCapture = false;
  4265. if (action === "mousewheel" && navigator.userAgent.indexOf("Firefox") >= 0) {
  4266. action = "DOMMouseScroll"; // For Firefox
  4267. }
  4268. element.removeEventListener(action, listener, useCapture);
  4269. } else {
  4270. // IE browsers
  4271. element.detachEvent("on" + action, listener);
  4272. }
  4273. };
  4274. /**
  4275. * Get HTML element which is the target of the event
  4276. * @param {MouseEvent} event
  4277. * @return {Element} target element
  4278. */
  4279. links.Timeline.getTarget = function (event) {
  4280. // code from http://www.quirksmode.org/js/events_properties.html
  4281. if (!event) {
  4282. event = window.event;
  4283. }
  4284. var target;
  4285. if (event.target) {
  4286. target = event.target;
  4287. }
  4288. else if (event.srcElement) {
  4289. target = event.srcElement;
  4290. }
  4291. if (target.nodeType !== undefined && target.nodeType == 3) {
  4292. // defeat Safari bug
  4293. target = target.parentNode;
  4294. }
  4295. return target;
  4296. };
  4297. /**
  4298. * Stop event propagation
  4299. */
  4300. links.Timeline.stopPropagation = function (event) {
  4301. if (!event)
  4302. event = window.event;
  4303. if (event.stopPropagation) {
  4304. event.stopPropagation(); // non-IE browsers
  4305. }
  4306. else {
  4307. event.cancelBubble = true; // IE browsers
  4308. }
  4309. };
  4310. /**
  4311. * Cancels the event if it is cancelable, without stopping further propagation of the event.
  4312. */
  4313. links.Timeline.preventDefault = function (event) {
  4314. if (!event)
  4315. event = window.event;
  4316. if (event.preventDefault) {
  4317. event.preventDefault(); // non-IE browsers
  4318. }
  4319. else {
  4320. event.returnValue = false; // IE browsers
  4321. }
  4322. };
  4323. /**
  4324. * Retrieve the absolute left value of a DOM element
  4325. * @param {Element} elem A dom element, for example a div
  4326. * @return {number} left The absolute left position of this element
  4327. * in the browser page.
  4328. */
  4329. links.Timeline.getAbsoluteLeft = function(elem) {
  4330. var left = 0;
  4331. while( elem != null ) {
  4332. left += elem.offsetLeft;
  4333. left -= elem.scrollLeft;
  4334. elem = elem.offsetParent;
  4335. }
  4336. if (!document.body.scrollLeft && window.pageXOffset) {
  4337. // FF
  4338. left -= window.pageXOffset;
  4339. }
  4340. return left;
  4341. };
  4342. /**
  4343. * Retrieve the absolute top value of a DOM element
  4344. * @param {Element} elem A dom element, for example a div
  4345. * @return {number} top The absolute top position of this element
  4346. * in the browser page.
  4347. */
  4348. links.Timeline.getAbsoluteTop = function(elem) {
  4349. var top = 0;
  4350. while( elem != null ) {
  4351. top += elem.offsetTop;
  4352. top -= elem.scrollTop;
  4353. elem = elem.offsetParent;
  4354. }
  4355. if (!document.body.scrollTop && window.pageYOffset) {
  4356. // FF
  4357. top -= window.pageYOffset;
  4358. }
  4359. return top;
  4360. };
  4361. /**
  4362. * Check if given object is a Javascript Array
  4363. * @param {*} obj
  4364. * @return {Boolean} isArray true if the given object is an array
  4365. */
  4366. // See http://stackoverflow.com/questions/2943805/javascript-instanceof-typeof-in-gwt-jsni
  4367. links.Timeline.isArray = function (obj) {
  4368. if (obj instanceof Array) {
  4369. return true;
  4370. }
  4371. return (Object.prototype.toString.call(obj) === '[object Array]');
  4372. };