Widget:Event-Timer/script.js: Unterschied zwischen den Versionen

Aus Guild Wars 2 Wiki
Zur Navigation springen Zur Suche springen
(Skript-Update mit der neusten Version aus EN)
KKeine Bearbeitungszusammenfassung
 
Zeile 1.227: Zeile 1.227:
function mainEventTimer(reloaded, paused) {
function mainEventTimer(reloaded, paused) {
   // Collect parameter options if specified
   // Collect parameter options if specified
   var zoneParameter = '<!--{$zone|default:""|escape:"javascript"}-->';
   // var zoneParameter = '<!--{$zone|default:""|escape:"javascript"}-->';
   var excludeParameter = '<!--{$exclude|default:""|escape:"javascript"}-->';
   // var excludeParameter = '<!--{$exclude|default:""|escape:"javascript"}-->';
  var excludeParameter = '';
 
  // Collect parameter options if specified
  var scriptNode = $('script#Widget_Event-Timer'), zoneParameter = '';
  if ( scriptNode.length > 0 ) {
    zoneParameter = scriptNode[0].getAttribute('data-zone');
  }


   // If the timer was reloaded via apply, or scrolled, reset event content and timers, otherwise its the first run and we need to create the preferences user interface.
   // If the timer was reloaded via apply, or scrolled, reset event content and timers, otherwise its the first run and we need to create the preferences user interface.

Aktuelle Version vom 8. Januar 2024, 13:37 Uhr

/*<nowiki>*/
/* Guild Wars 2 Wiki: Event timer */
// Increment this every time a release is added to invalidate the existing sequence and force users to load the new map timer.
var version = 'v3.7.0'; // November 2023: Added logic path to reset sequence if all non-toptime event bars have been removed using the [x].

// GLOBAL VARIABLES
// User interface buttons, labels, checkboxes
var uitext = {
  widgetlink: "Widget:Event timer",
  widgetlinktext: "Dokumentation",
  timezonehover: "Dies ist deine Zeitzone",
  timeshiftresume: "Liveupdate aus - Hier klicken zum Aktivieren",
  timeshiftnexthoverpause: "Klicken um Liveupdate zu pausieren und zwei Stunden weiter zu gehen",
  timeshiftprevhover: "Klicken um zu den vorherigen zwei Stunden zu gehen",
  timeshiftnexthover: "Klicken um zu den folgenden zwei Stunden zu gehen",
  checkboxhover: "Klicke auf Anwenden um die Einstellungen zu speichern.",
  legendname: "Einstellungen für Event-Timer",
  applysettings: "Einstellungen anwenden",
  forgetsettings: "Einstellungen zurücksetzen",
  deleterowhover: "Versteckt diese Zeile. Setze die Einstellungen zurück, um alle Zeilen wieder anzuzeigen.",

  checkboxes: {
    twelvehour: {
      name: "12-Stunden-Uhrzeit anzeigen.",
      hover: "Wenn ausgewählt, werden Uhrzeiten im 12-Stunden-Format mit AM/PM angezeigt.",
      defaultvalue: false
    },

    toptimes: {
      name: "Kompakte Darstellung.",
      hover: "Wenn ausgewählt, wird der Timer kompakter dargestellt.",
      defaultvalue: true
    },

    compact: {
      name: "Kompakte Überschriftendarstellung.",
      hover: "Wenn ausgewählt, wird der Timer kompakter dargestellt, da die Überschrften links statt über dem Timer erscheinen. Keinen Effekt wenn Überschriften versteckt wurden.",
      defaultvalue: true
    },

    hidecategories: {
      name: "Kategorien verstecken.",
      hover: "Wenn ausgewählt, wird der Timer durch das Entfernen der Kategorienüberschriften kompakter dargestellt.",
      defaultvalue: false
    },

    hideheadings: {
      name: "Überschriften verstecken.",
      hover: "Wenn ausgewählt, wird der Timer durch das Entfernen der Karten-Meta-Überschriften kompakter dargestellt.",
      defaultvalue: false
    },

    hidechatlinks: {
      name: "Chatlinks verstecken.",
      hover: "Wenn ausgewählt, werden keine Chatlinks angezeigt.",
      defaultvalue: false
    },

    even: {
      name: "Nur zu geraden UTC-Stunden starten.",
      hover: "Wenn ausgewählt, beginnt der Timer immer zur vorherigen geraden UTC-Stunde.",
      defaultvalue: false
    }
  }
};

// Event names, schedules, colours
var eventData = {
  // Time
  t: {
    name: "",
    segments: {
      0: { name: "", bg: "transparent" }
    },
    sequences: {
      partial: [],
      pattern: [{r:0,d:15}]
    }
  },

  // ** Zentraltyria **
  dn: {
    category: "Zentraltyria",
    name: "Tageszeit",
    segments: {
      1: { name: "Tag", link: "Tageszeit", bg: [255,255,255] },
      2: { name: "Dämmerung", link: "Tageszeit", bg: [[255,255,255],[122,134,171]] },
      3: { name: "Nacht", link: "Tageszeit", bg: [122,134,171] },
      4: { name: "Morgengrauen", link: "Tageszeit", bg: [[122,134,171],[255,255,255]] }
    },
    sequences: {
      partial: [{r:3,d:25},{r:4,d:5}],
      pattern: [{r:1,d:70},{r:2,d:5},{r:3,d:40},{r:4,d:5}]
    }
  },

  wb: {
    category: "Zentraltyria",
    name: "Welt-Bosse",
    link: "Welt-Boss",
    segments: {
      1: { name: "Admiral Taidha Covington", link: "Die Kampagne gegen Taidha Covington", chatlink: "[&BKgBAAA=]", bg: [ 66,200,215] },
      2: { name: "Klaue von Jormag", link: "Zerstörung der Klaue Jormags", chatlink: "[&BHoCAAA=]", bg: [ 66,200,215] },
      3: { name: "Feuer-Elementar", link: "Thaumanova-Reaktor-Fallout", chatlink: "[&BEcAAAA=]", bg: [138,234,244] },
      4: { name: "Inquestur-Golem Typ II", link: "Besiegt den Inquestur-Golem Typ II", chatlink: "[&BNQCAAA=]", bg: [ 66,200,215] },
      5: { name: "Großer Dschungelwurm", link: "Bezwingt den großen Dschungelwurm", chatlink: "[&BEEFAAA=]", bg: [138,234,244] },
      6: { name: "Mega-Zerstörer", link: "Die Schlacht um den Mahlstromgipfel", chatlink: "[&BM0CAAA=]", bg: [ 66,200,215] },
      7: { name: "Modniir Ulgoth", link: "Bezwingt_Ulgoth_den_Modniir_und_seine_Diener", chatlink: "[&BLAAAAA=]", bg: [ 66,200,215] },
      8: { name: "Schatten-Behemoth", link: "Geheimnisse im Sumpf", chatlink: "[&BPcAAAA=]", bg: [138,234,244] },
      9: { name: "Svanir-Schamane", link: "Der gefrorene Schlund", chatlink: "[&BMIDAAA=]", bg: [138,234,244] },
      10: { name: "Der Zerschmetterer", link: "Kralkatorriks Vermächtnis", chatlink: "[&BE4DAAA=]", bg: [ 66,200,215] }
    },
    sequences: {
      partial: [],
      pattern: [{r:1,d:15},{r:9,d:15},{r:6,d:15},{r:3,d:15},{r:10,d:15},{r:5,d:15},{r:7,d:15},{r:8,d:15},{r:4,d:15},{r:9,d:15},{r:2,d:15},{r:3,d:15},{r:1,d:15},{r:5,d:15},{r:6,d:15},{r:8,d:15},{r:10,d:15},{r:9,d:15},{r:7,d:15},{r:3,d:15},{r:4,d:15},{r:5,d:15},{r:2,d:15},{r:8,d:15}]
    }
  },

  hwb: {
    category: "Zentraltyria",
    name: "Hardcore Welt-Bosse",
    link: "Welt-Boss",
    segments: {
      0: { name: "", bg: [138,234,244] },
      1: { name: "Dreifacher Ärger", link: "Dreifacher Ärger", chatlink: "[&BKoBAAA=]", bg: [ 66,200,215] },
      2: { name: "Karka-Königin", link: "Inselkontrolle", chatlink: "[&BNUGAAA=]", bg: [ 66,200,215] },
      3: { name: "Tequatl der Sonnenlose", link: "Besiegt Tequatl den Sonnenlosen", chatlink: "[&BNABAAA=]", bg: [ 66,200,215] }
    },
    sequences: {
      partial: [{r:3,d:30},{r:0,d:30},{r:1,d:30},{r:0,d:30},{r:2,d:30},{r:0,d:30},{r:3,d:30},{r:0,d:30},{r:1,d:30},{r:0,d:90},{r:2,d:30},{r:0,d:30},{r:3,d:30},{r:0,d:30},{r:1,d:30},{r:0,d:120},{r:2,d:30},{r:0,d:30},{r:3,d:30},{r:0,d:30},{r:1,d:30},{r:0,d:120},{r:2,d:30},{r:0,d:30},{r:3,d:30},{r:0,d:30},{r:1,d:30},{r:0,d:30},{r:2,d:30},{r:0,d:30},{r:3,d:30},{r:0,d:30},{r:1,d:30},{r:0,d:150},{r:2,d:30},{r:0,d:30},{r:3,d:30},{r:0,d:30},{r:1,d:30}],
      pattern: []
    }
  },

  la: {
    category: "Zentraltyria",
    name: "Ley-Linien-Anomalie",
    link: "Legende Ley-Linien-Anomalie",
    segments: {
      0: { name: "", bg: [251,132,152] },
      1: { name: "Baumgrenzen-Fälle", link: "Besiegt_die_Ley-Linien-Anomalie,_um_ihre_zerstörerische_Energie_aufzulösen,_bevor_sie_überlädt_(Baumgrenzen-Fälle)", chatlink: "[&BEwCAAA=]", bg: [215, 66, 91] },
      2: { name: "Eisenmark", link: "Besiegt_die_Ley-Linien-Anomalie,_um_ihre_zerstörerische_Energie_aufzulösen,_bevor_sie_überlädt_(Eisenmark)", chatlink: "[&BOYBAAA=]", bg: [215, 66, 91] },
      3: { name: "Gendarran-Felder", link: "Besiegt_die_Ley-Linien-Anomalie,_um_ihre_zerstörerische_Energie_aufzulösen,_bevor_sie_überlädt_(Gendarran-Felder)", chatlink: "[&BO0AAAA=]", bg: [215, 66, 91] }
    },
    sequences: {
      partial: [{r:0,d:20},{r:1,d:20},{r:0,d:100},{r:2,d:20},{r:0,d:100},{r:3,d:20}],
      pattern: [{r:0,d:100},{r:1,d:20},{r:0,d:100},{r:2,d:20},{r:0,d:100},{r:3,d:20}]
    }
  },


            pvpat: {
                category: "Zentraltyria",
                name: "PvP-Turniere",
                link: "Automatisierte Turniere",
                segments: {
                    0: { name: "", bg: [251,132,152] },
                    1: { name: "Automatisiertes Turnier: Balthasars Rauferei", link: "Automatisierte_Turniere#Tägliches_Turnier", bg: [234, 98,121] },
                    2: { name: "Automatisiertes Turnier: Grenths Gastspiel", link: "Automatisierte_Turniere#Tägliches_Turnier", bg: [234, 98,121] },
                    3: { name: "Automatisiertes Turnier: Melandrus Begegnung", link: "Automatisierte_Turniere#Tägliches_Turnier", bg: [234, 98,121] },
                    4: { name: "Automatisiertes Turnier: Lyssas Legionen", link: "Automatisierte_Turniere#Tägliches_Turnier", bg: [234, 98,121] }
                },
                sequences: {
                    partial: [],
                    pattern: [{r:1,d:60},{r:0,d:120},{r:2,d:60},{r:0,d:120},{r:3,d:60},{r:0,d:120},{r:4,d:60},{r:0,d:120}]
                }
            },

  eotn: {
    category: "Lebendige Welt Staffel 1",
    name: "Auge des Nordens",
    link: "Auge des Nordens",
    segments: {
      0: { name: "", bg: [251,132,152] },
      1: { name: "Die Verdrehte Marionette (Öffentlich)", link: "Die Verdrehte Marionette", chatlink: "[&BAkMAAA=]", bg: [234, 98,121] },
      2: { name: "Tower of Nightmares (Public)", link: "The Tower of Nightmares (meta event)", chatlink: "[&BAkMAAA=]", bg: [234, 98,121] },
      3: { name: "Battle For Lion's Arch (Public)", link: "The Battle For Lion's Arch", chatlink: "[&BAkMAAA=]", bg: [234, 98,121] }
    },
    sequences: {
      partial: [],
      pattern: [{r:1,d:20},{r:0,d:10},{r:3,d:15},{r:0,d:45},{r:2,d:15},{r:0,d:15}]
    }
  },

            si: {
                category: "Lebendige Welt Staffel 1",
                name: "Scarlets Invasion",
                link: "Besiegt die eindringenden Diener von Scarlet Dornstrauch",
                segments: {
                    0: { name: "", bg: [251,132,152] },
                    1: { name: "Besiegt Scarlets Diener", link: "Besiegt die eindringenden Diener von Scarlet Dornstrauch", chatlink: "[&BOQAAAA=]", bg: [234, 98,121] }
                },
                sequences: {
                    partial: [{r:0,d:60},{r:1,d:15}],
                    pattern: [{r:0,d:105},{r:1,d:15}]
                }
            },

 
  // ** Lebendige Welt Staffel 2 **
  dt: {
    category: "Lebendige Welt Staffel 2",
    name: "Trockenkuppe",
    segments: {
      1: { name: "Absturzstelle", bg: [251,227,132] },
      2: { name: "Sandsturm!", chatlink: "[&BIAHAAA=]", bg: [215,185, 66] }
    },
    sequences: {
      partial: [],
      pattern: [{r:1,d:40},{r:2,d:20}]
    }
  },

  // ** Heart of Thorns **
  vb: {
    category: "Heart of Thorns",
    name: "Grasgrüne Schwelle",
    segments: {
      1: { name: "Sicherung der Grasgrünen Schwelle", link: "Grasgrüne Schwelle#Tagsüber", bg: [231,251,132] },
      2: { name: "Die Nacht und der Feind", bg: [211,234, 98] },
      3: { name: "Nachtbosse", link: "Die Nacht und der Feind", chatlink: "[&BAgIAAA=]", bg: [190,215, 66] }
    },
    sequences: {
      partial: [{r:2,d:10},{r:3,d:20}],
      pattern: [{r:1,d:75},{r:2,d:25},{r:3,d:20}]
    }
  },

  ab: {
    category: "Heart of Thorns",
    name: "Güldener Talkessel",
    segments: {
      1: { name: "Pylonen", link: "Die Verteidigung von Tarir", chatlink: "[&BN0HAAA=]", bg: [231,251,132] },
      2: { name: "Herausforderungen", link: "Feuerprobe", chatlink: "[&BGwIAAA=]", bg: [211,234, 98] },
      3: { name: "Rankenkrake", link: "Schlacht in Tarir", chatlink: "[&BAIIAAA=]", bg: [190,215, 66] },
      4: { name: "Reset", link: "Eine kurze Verschnaufpause", bg: [211,234, 98] }
    },
    sequences: {
      partial: [{r:1,d:45},{r:2,d:15},{r:3,d:20},{r:4,d:10}],
      pattern: [{r:1,d:75},{r:2,d:15},{r:3,d:20},{r:4,d:10}]
    }
  },

  td: {
    category: "Heart of Thorns",
    name: "Verschlungene Tiefen",
    segments: {
      1: { name: "Helft den Außenposten", link: "Verschlungene_Tiefen#Meta-Events", bg: [231,251,132] },
      2: { name: "Vorbereitung", link: "König des Dschungels", bg: [211,234, 98] },
      3: { name: "Chak-Potentat", link: "König des Dschungels", chatlink: "[&BPUHAAA=]", bg: [190,215, 66] }
    },
    sequences: {
      partial: [{r:1,d:25},{r:2,d:5},{r:3,d:20}],
      pattern: [{r:1,d:95},{r:2,d:5},{r:3,d:20}]
    }
  },

  ds: {
    category: "Heart of Thorns",
    name: "Widerstand des Drachen",
    segments: {
      1: { name: "Start des Angriffs", link: "Widerstand des Drachen (Meta-Event)", chatlink: "[&BBAIAAA=]", bg: [190,215, 66] },
      2: { name: "(fortgesetzt)", link: "Widerstand des Drachen (Meta-Event)", bg: [190,215, 66] }
    },
    sequences: {
      partial: [{r:2,d:90}],
      pattern: [{r:1,d:120}]
    }
  },

  // ** Lebendige Welt Staffel 3 **
  ld: {
    category: "Lebendige Welt Staffel 3",
    name: "Doric-See",
    segments: {
      1: { name: "Saidras Hafen", link: "Kontrolle des Weißen Mantels: Saidras Hafen", chatlink: "[&BK0JAAA=]", bg: [251,132,152] },
      2: { name: "Neulehmwald", link: "Kontrolle des Weißen Mantels: Neulehmwald", chatlink: "[&BLQJAAA=]", bg: [234, 98,121] },
      3: { name: "Norans Heimstatt", link: "Kontrolle des Weißen Mantels: Norans Heimstatt", chatlink: "[&BK8JAAA=]", bg: [215, 66, 91] }
    },
    sequences: {
      partial: [{r:2,d:30}],
      pattern: [{r:3,d:30},{r:1,d:45},{r:2,d:45}]
    }
  },

  // ** Path of Fire **
  co: {
    category: "Path of Fire",
    name: "Kristalloase",
    segments: {
      0: { name: "", bg: [251,199,132] },
      1: { name: "Runde 1 bis 3", link: "Kasino-Blitz", chatlink: "[&BLsKAAA=]", bg: [234,175, 98] },
      2: { name: "Piñata/Reset", link: "Kasino-Blitz", chatlink: "[&BLsKAAA=]", bg: [215,150, 66] }
    },
    sequences: {
      partial: [{r:0,d:5},{r:1,d:16},{r:2,d:9}],
      pattern: [{r:0,d:95},{r:1,d:16},{r:2,d:9}]
    }
  },

  dh: {
    category: "Path of Fire",
    name: "Wüsten-Hochland",
    segments: {
      0: { name: "", bg: [251,199,132] },
      1: { name: "Vergrabene Schätze", link: "Die Suche nach vergrabenen Schätzen", chatlink: "[&BGsKAAA=]", bg: [234,175, 98] }
    },
    sequences: {
      partial: [{r:0,d:60},{r:1,d:20}],
      pattern: [{r:0,d:100},{r:1,d:20}]
    }
  },

  er: {
    category: "Path of Fire",
    name: "Elon-Flusslande",
    segments: {
      0: { name: "", bg: [251,199,132] },
      1: { name: "Fels der Weissagung", link: "Der Pfad zum Aufstieg", chatlink: "[&BFMKAAA=]", bg: [234,175, 98] },
      2: { name: "Doppelgänger", link: "Der Pfad zum Aufstieg", chatlink: "[&BCgKAAA=]", bg: [215,150, 66] }
    },
    sequences: {
      partial: [{r:2,d:15}],
      pattern: [{r:0,d:75},{r:1,d:25},{r:2,d:20}]
    }
  },

  de: {
    category: "Path of Fire",
    name: "Das Ödland",
    segments: {
      0: { name: "", bg: [251,199,132] },
      1: { name: "Schlünde der Qual", chatlink: "[&BKMKAAA=]", bg: [215,150, 66] },
      2: { name: "Aufstand der Junundu", chatlink: "[&BMEKAAA=]", bg: [234,175, 98] }
    },
    sequences: {
      partial: [{r:0,d:30},{r:2,d:20},{r:0,d:10}],
      pattern: [{r:1,d:20},{r:0,d:10},{r:2,d:20},{r:0,d:40},{r:2,d:20},{r:0,d:10}]
    }
  },

  dv: {
    category: "Path of Fire",
    name: "Domäne Vaabi",
    segments: {
      0: { name: "", bg: [251,199,132] },
      1: { name: "Zorn der Schlangen", chatlink: "[&BHQKAAA=]", bg: [234,175, 98] },
      2: { name: "Im Feuer geschmiedet", chatlink: "[&BO0KAAA=]", bg: [215,150, 66] }
    },
    sequences: {
      partial: [{r:2,d:30}],
      pattern: [{r:1,d:30},{r:2,d:30},{r:0,d:30},{r:2,d:30}]
    }
  },

  // ** Lebendige Welt Staffel 4 **
            ai: {
                category: "Lebendige Welt Staffel 4",
                name: "Eindringende Erweckte",
                segments: {
                    0: { name: "", bg: [187,119,207] },
                    1: { name: "Eindringende Erweckte", link: "Besiegt die eindringenden Erweckten", bg: [157,65,185] },
                },
                sequences: {
                    partial: [{r:0,d:30}],
                    pattern: [{r:1,d:15},{r:0,d:45}]
                }
            },

  di: {
    category: "Lebendige Welt Staffel 4",
    name: "Domäne Istan",
    segments: {
      0: { name: "", bg: [187,119,207] },
      1: { name: "Palawadan", link: "Palawadan, Juwel von Istan (Meta-Event)", chatlink: "[&BAkLAAA=]", bg: [157,65,185] },
    },
    sequences: {
      partial: [{r:1,d:15}],
      pattern: [{r:0,d:90},{r:1,d:30}]
    }
  },

  jb: {
    category: "Lebendige Welt Staffel 4",
    name: "Jahai-Klippen",
    segments: {
      0: { name: "", bg: [187,119,207] },
      1: { name: "Eskorte", link: "Eskortiert die DERV zu den Gräben des Zerschmetterers", chatlink: "[&BIMLAAA=]", bg: [175, 96,199] },
      2: { name: "Zerschmetterer", link: "Vernichtet den Todesgebrandmarkten Zerschmetterer", chatlink: "[&BJMLAAA=]", bg: [157,65,185] },
    },
    sequences: {
      partial: [{r:0,d:60},{r:1,d:15},{r:2,d:15}],
      pattern: [{r:0,d:90},{r:1,d:15},{r:2,d:15}]
    }
  },

  tp: {
    category: "Lebendige Welt Staffel 4",
    name: "Donnerkopf-Gipfel",
    segments: {
      0: { name: "", bg: [187,119,207] },
      1: { name: "Feste Donnerkopf", link: "Feste Donnerkopf (Meta-Event)", chatlink: "[&BLsLAAA=]", bg: [157,65,185] },
      2: { name: "Öl auf dem Eis", chatlink: "[&BKYLAAA=]", bg: [157,65,185] },
    },
    sequences: {
      partial: [{r:1,d:5},{r:0,d:40},{r:2,d:15}],
      pattern: [{r:0,d:45},{r:1,d:20},{r:0,d:40},{r:2,d:15}]
    }
  },

  // ** The Icebrood Saga **
  gv: {
    category: "Die Eisbrut-Saga",
    name: "Grothmar-Tal",
    segments: {
      0: { name: "", bg: [132,201,251] },
      1: { name: "Flammenabbild", link: "Zeremonie der Heiligen Flamme", chatlink: "[&BA4MAAA=]", bg: [ 98,177,234] },
      2: { name: "Schrein", link: "Heimsuchung des Schreins des Schicksalwissens", chatlink: "[&BA4MAAA=]", bg: [ 66,153,215] },
      3: { name: "Schleimgrube", link: "Schleimgruben-Prüfungen", chatlink: "[&BPgLAAA=]", bg: [ 98,177,234] },
      4: { name: "Konzert", link: "Ein Konzert für die Ewigkeit", chatlink: "[&BPgLAAA=]", bg: [ 66,153,215] }
    },
    sequences: {
      partial: [{r:0,d:10}],
      pattern: [{r:1,d:15},{r:0,d:13},{r:2,d:22},{r:0,d:5},{r:3,d:20},{r:0,d:15},{r:4,d:15},{r:0,d:15}]
    }
  },
  
  bm: {
    category: "Die Eisbrut-Saga",
    name: "Bjora-Sümpfe",
    segments: {
      0: { name: "", bg: [132,201,251] },
      1: { name: "Drakkar und die Geister der Wildnis", link: "Champion des Eisdrachen", chatlink: "[&BDkMAAA=]", bg: [ 66,153,215] },
      2: { name: "Rabenschreine", link: "Stürme des Winters", chatlink: "[&BCcMAAA=]", bg: [ 98,177,234] },
      3: { name: "Scherben und Konstrukt", link: "Stürme des Winters", chatlink: "[&BCcMAAA=]", bg: [ 66,153,215] },
      4: { name: "Eisbrut Champions", link: "Stürme des Winters", chatlink: "[&BCcMAAA=]", bg: [ 98,177,234] }
    },
    sequences: {
      partial: [{r:3,d:5},{r:4,d:15}],
      pattern: [{r:0,d:45},{r:1,d:35},{r:0,d:5},{r:2,d:15},{r:3,d:5},{r:4,d:15}]
    }
  },

  dsp: {
    category: "Die Eisbrut-Saga",
    name: "Drachensturm",
    segments: {
      0: { name: "", bg: [132,201,251] },
      1: { name: "Drachensturm", link: "Drachensturm", chatlink: "[&BAkMAAA=]", bg: [ 66,153,215] }
    },
    sequences: {
      partial: [{r:0,d:60}],
      pattern: [{r:1,d:20},{r:0,d:100}]
    }
  },

  // ** End of Dragons **
  cdn: {
      category: "End of Dragons",
      name: "Cantha: Tageszeit",
      segments: {
          1: { name: "Tag", link: "Tageszeit", bg: [255,255,255] },
          2: { name: "Dämmerung", link: "Tageszeit", bg: [[255,255,255],[122,134,171]] },
          3: { name: "Nacht", link: "Tageszeit", bg: [122,134,171] },
          4: { name: "Morgengrauen", link: "Tageszeit", bg: [[122,134,171],[255,255,255]] }
      },
      sequences: {
          partial: [{r:3,d:35},{r:4,d:5}],
          pattern: [{r:1,d:55},{r:2,d:5},{r:3,d:55},{r:4,d:5}]
      }
  },
  sp: {
      category: "End of Dragons",
      name: "Provinz Seitung",
      segments: {
          0: { name: "", bg: [195,255,245] },
          1: { name: "Ätherklingen-Angriff", chatlink: "[&BGUNAAA=]", bg: [90,243,222] }
      },
      sequences: {
          partial: [{r:0,d:90}],
          pattern: [{r:1,d:30},{r:0,d:90}]
      }
  },
  nkc: {
      category: "End of Dragons",
      name: "Stadt Neu-Kaineng",
      segments: {
          0: { name: "", bg: [195,255,245] },
          1: { name: "Kaineng-Energieausfall", chatlink: "[&BBkNAAA=]", bg: [90,243,222] }
      },
      sequences: {
          partial: [],
          pattern: [{r:1,d:30},{r:0,d:90}]
      }
  },
  tew: {
      category: "End of Dragons",
      name: "Die Echowald-Wildnis",
      segments: {
          0: { name: "", bg: [138,234,244] },
          1: { name: "Bandenkrieg", link: "Der Bandenkrieg von Echowald", chatlink: "[&BMwMAAA=]", bg: [ 66,200,215] },
          2: { name: "Espenwald", link: "Zerstört mithilfe der der Belagerungsschildkröten die Schildgeneratoren, während Ihr Euch durch das Fort kämpft", chatlink: "[&BPkMAAA=]", bg: [ 96,220,235] }
      },
      sequences: {
			        partial: [],
			        pattern: [{r:0,d:30},{r:1,d:35},{r:0,d:35},{r:2,d:20}]
      }
  },
  dre: {
      category: "End of Dragons",
      name: "Drachen-Ende",
      segments: {
          1: { name: "Vorbereitungen", chatlink: "[&BKIMAAA=]", bg: [195,255,245] },
          2: { name: "Die Schlacht ums Jademeer", chatlink: "[&BKIMAAA=]", bg: [90,243,222] }
      },
      sequences: {
          partial: [],
          pattern: [{r:1,d:60},{r:2,d:60}]
      }
  },

            // ** Secrets of the Obscure **
            sa: {
                category: "Secrets of the Obscure",
                name: "Himmelswacht-Archipel",
                segments: {
                    0: { name: "", bg: [250,206,133] },
                    1: { name: "Den Turm des Zauberers entriegeln", link: "Den Turm des Zauberers entriegeln", chatlink: "[&BL4NAAA=]", bg: [210,155, 73] }
                },
                sequences: {
                    partial: [{r:0,d:60}],
                    pattern: [{r:1,d:25},{r:0,d:95}]
                }
            },
            wt: {
                category: "Secrets of the Obscure",
                name: "Der Turm des Zauberers",
                segments: {
                    0: { name: "", bg: [250,206,133] },
                    1: { name: "Himmelsschuppen-Zielübungen", link: "Himmelsschuppen-Zielübungen im Turm des Zauberers", chatlink: "[&BB8OAAA=]", bg: [226,171, 73] },
                    2: { name: "Nachtflug", link: "Turm des Zauberers: Nachtflug", chatlink: "[&BB8OAAA=]", bg: [226,171, 73] },
                    3: { name: "Himmelsschuppen-Zielübungen & Nachtflug", link: "Abenteuer#Secrets of the Obscure", chatlink: "[&BB8OAAA=]", bg: [200,136, 54] }
                },
                sequences: {
                    partial: [{r:2,d:20},{r:0,d:40}],
                    pattern: [{r:1,d:40},{r:3,d:15},{r:2,d:25},{r:0,d:40}]
                }
            },
           
            am: {
                category: "Secrets of the Obscure",
                name: "Amnytas",
                segments: {
                    0: { name: "", bg: [250,206,133] },
                    1: { name: "Verteidigung von Amnytas", link: "Die Verteidigung von Amnytas", chatlink: "[&BDQOAAA=]", bg: [210,155, 73] }
                },
                sequences: {
                    partial: [],
                    pattern: [{r:1,d:25},{r:0,d:95}]
                 }
             },

            con: {
                category: "Secrets of the Obscure",
                name: "Konvergenz (Instanz)",
                segments: {
                    0: { name: "", bg: [250,206,133] },
                    1: { name: "Konvergenzen", link: "Konvergenz (Instanz)", chatlink: "[&BB8OAAA=]", bg: [226,171, 73] }
                },
                sequences: {
                    partial: [{r:0,d:90}],
                    pattern: [{r:1,d:10},{r:0,d:170}]
                }
            },

            // ** Special Events **
            lc: {
                category: "Spezialevents",
                name: "Labyrinthklippen",
                segments: {
                    0: { name: "", bg: [138,234,244] },
                    1: { name: "Skiff-Rennen", link: "Labyrinth-Skiffe: Bald fängt ein Rennen an", chatlink: "[&BBwHAAA=]", bg: [ 66,200,215] },
                    2: { name: "Schatzjagd", link: "Sammelt vor Ablauf der Zeit so viel Beute wie möglich!", chatlink: "[&BBwHAAA=]", bg: [ 66,200,215] },
                    3: { name: "Schweberochen-Rennen", link: "Schwebe-Rochen-Slalom: Erreicht die Ziellinie", chatlink: "[&BBwHAAA=]", bg: [ 66,200,215] },
                    4: { name: "Angeln", link: "Anmeldung zum Angelturnier", chatlink: "[&BBwHAAA=]", bg: [ 66,200,215] },
                    5: { name: "Dolyak-Rennen", link: "Fliegender Dolyak: Erreicht die Ziellinie", chatlink: "[&BBwHAAA=]", bg: [ 66,200,215] }
                },
                sequences: {
                    partial: [],
                    pattern: [{r:1,d:10},{r:0,d:20},{r:2,d:30},{r:0,d:15},{r:3,d:10},{r:0,d:5},{r:4,d:10},{r:0,d:5},{r:5,d:10},{r:0,d:5}]
                }
            },

            db: {
                category: "Spezialevents",
                name: "Drachen-Gepolter",
                segments: {
                    0: { name: "", bg: [138,234,244] },
                    1: { name: "Wanderer-Hügel", link: "Hologramm-Ansturm des Drachen-Gepolters", chatlink: "[&BH0BAAA=]", bg: [ 66,200,215] },
                    2: { name: "Schauflerschreck-Klippen", link: "Hologramm-Ansturm des Drachen-Gepolters", chatlink: "[&BGMCAAA=]", bg: [ 66,200,215] },
                    3: { name: "Lornars Pass", link: "Hologramm-Ansturm des Drachen-Gepolters", chatlink: "[&BJkBAAA=]", bg: [ 66,200,215] },
                    4: { name: "Schneekuhlenhöhen", link: "Hologramm-Ansturm des Drachen-Gepolters", chatlink: "[&BL4AAAA=]", bg: [ 66,200,215] }
                },
                sequences: {
                    partial: [],
                    pattern: [{r:1,d:5},{r:0,d:10},{r:2,d:5},{r:0,d:10},{r:3,d:5},{r:0,d:10},{r:4,d:5},{r:0,d:10}]
                }
            },

            ha: {
                category: "Spezialevents",
                name: "Halloween",
                segments: {
                    0: { name: "", bg: [242,215,162] },
                    1: { name: "Der Verrückte König sagt", link: "Der Verrückte König sagt:", chatlink: "[&BBAEAAA=]", bg: [232,163,31] }
                },
                sequences: {
                    partial: [],
                    pattern: [{r:1,d:10},{r:0,d:110}]
                }
            }
};

// Placeholder object which will become a copy of eventData, but only with the metas specified in defaultSequence.
var customEventData = {};

// Sequence in which the elements will render.
var defaultSequence = Object.keys(eventData);

// If there are more than 10 elements showing, it's probably a long way between the first times and the last, so add another to the end.
if (defaultSequence.length > 10) {
  defaultSequence.push('t');
}

// Calculate the user timezone offset for continued later use. Globally track start hour too.
var now = new Date(), timezoneOffset = (-1 * now.getTimezoneOffset()), startHourUTC, twelveHourTimes, setIntervalHandle, otherHourOffset = 0, usedHeadings = [];

// UTILITY FUNCTIONS
// Utility function #1: Write CSS
function writeTimerCSS() {
  // ** Sheet 2 - Event colour scheme **
  var cssText = $.map(eventData, function (metaEventData, metaKey) {
    var x;
    return $.map(metaEventData.segments, function (v, k) {
      x = '';
      switch (typeof v.bg) {
        case 'object':
          switch (v.bg.length) {
            case 2: // linear-gradients
              x = '.event-bar-segment.' + metaKey + k + ' { background: linear-gradient(90deg, rgb(' + v.bg[0].join(',') + '), rgb(' + v.bg[1].join(',') + ')) }';
              break;
            case 3:
              x = ['.event-bar-segment.' + metaKey + k + ' { background: rgb(' + v.bg.join(',') + ') }',
              '.event-bar-segment.' + metaKey + k + '.future { background: rgba(' + v.bg.join(',') + ',0.3) }'];
              break;
          }
          break;
        case 'string': // transparent or other alternative text
          x = '.event-bar-segment.' + metaKey + k + ' { background: ' + v.bg + '}';
          break;
      }
      return x;
    });
  }).join('\n');
  $('#EventTimerCSS2').text('/* Widget:Event timer - Stylesheet 1 */\n' + cssText);

  // ** Sheet 3 - Compact window width **
  // Run once
  fitTimerToWindowWidth();

  // And rerun when the window changes size
  var resizeTimer;
  $(window).resize(function () {
    clearTimeout(resizeTimer);
    resizeTimer = setTimeout(fitTimerToWindowWidth, 250);
  });
}

// Utility function #2 and #3: HTML5 localStorage operator functions used to request existing preferences, and store user preferences for later visits
function getEventTimerPreferences(keyname, defaultvalue) {
  var response = JSON.parse(localStorage.getItem('event-timer-' + keyname));
  if (typeof response == 'undefined' || response == null) { response = defaultvalue; }
  return response;
}
function setEventTimerPreferences(keyname, value, defaultvalue) {
  switch (typeof value) {
    case 'string':
    case 'object':
    case 'boolean':
      break;
    default:
      console.log('Invalid preference ignored:', value);
      value = defaultvalue;
      break;
  }
  try {
    localStorage.removeItem('event-timer-' + keyname);
    localStorage.setItem('event-timer-' + keyname, JSON.stringify(value));
    console.log('Changed preference: ', keyname);
  }
  catch (e) {
    console.log('localStorage not supported (HTML5 browser required)');
  }
}

// Utility function #4: Create a legend with checkboxes for viewers to set their preferences.
function eventTimerPreferences() {
  function addCheckbox(keyname, desc, hoverdesc, defaultvalue) {
    hoverdesc += ' >> ' + uitext.checkboxhover;
    var box = $(document.createElement("input")).attr("type", "checkbox").attr("id", keyname + "-toggle").attr("title", hoverdesc);
    var label = $(document.createElement("label")).attr("for", keyname + "-toggle").attr("title", hoverdesc).text(desc);
    box.attr('checked', getEventTimerPreferences(keyname, defaultvalue));
    eventTimerSettings.append(box).append(label);
  }
  // Create fieldset container with legend
  var eventTimerSettings = $(document.createElement("fieldset")).attr("class", "widget").attr("id", "event-timer-legend")
    .append($(document.createElement("legend")).text(uitext.legendname));
  $.each(uitext.checkboxes, function (k, v) {
    addCheckbox(k, v.name, v.hover, v.defaultvalue);
  });
  eventTimerSettings.append($(document.createElement("input")).attr("id", "apply-button").attr("class", "mw-ui-button button").attr("type", "button").attr("value", uitext.applysettings));
  eventTimerSettings.append($(document.createElement("input")).attr("id", "forget-button").attr("class", "mw-ui-button button").attr("type", "button").attr("value", uitext.forgetsettings));
  eventTimerSettings.append($(document.createElement("span"))
    .append(wikiLink(uitext.widgetlink, uitext.widgetlinktext))
  );
  $('#event-wrapper').after(eventTimerSettings);

  // Save checkbox settings
  $.each(uitext.checkboxes, function (k, v) {
    $('#' + k + '-toggle').click(function () {
      setEventTimerPreferences(k, $('#' + k + '-toggle').prop('checked'), v.defaultvalue);
    });
  });
  $('#apply-button').click(function () {
    mainEventTimer(true);
  });
  $('#forget-button').click(function () {
    try {
      // Remove local storage and reset checkboxes
      localStorage.removeItem('event-timer-version');
      localStorage.removeItem('event-timer-sequence');
      $.each(uitext.checkboxes, function (k, v) {
        localStorage.removeItem('event-timer-' + k);
        $('#' + k + '-toggle').prop('checked', v.defaultvalue);
      });
      console.log('Removed stored event timer preferences.');
    } catch (e) {
      console.log('localStorage not supported (HTML5 browser required)');
    }
    mainEventTimer(true);
  });
}

// Utility function #5: Convert a time given in minutes since 00:00 into a recognizable time. (1440 = one whole day, 1515 = one whole schedule day)
function unwrapUTC(time) {
  var timeRaw, timeString;

  // Check combined offset in hours is not beyond 23:59
  time = time % 1440;

  // Calculate the hours and minutes
  var hour = Math.floor(time / 60);
  var minute = time % 60;

  // If timezone offset is zero, use UTC time and don't bother with date objects, otherwise use local time
  if (timezoneOffset == 0) {
    if (twelveHourTimes == false) {
      timeRaw = pad(hour) + ':' + pad(minute);
      timeString = pad(hour) + ':' + pad(minute);
    } else {
      timeRaw = (((hour + 11) % 12) + 1) + ':' + pad(minute) + ' ' + (hour >= 12 ? 'PM' : 'AM');
      timeString = (((hour + 11) % 12) + 1) + ':' + pad(minute) + ' ' + (hour >= 12 ? 'PM' : 'AM');
    }
  } else {
    var date = new Date();
    date.setUTCHours(hour, minute, 0, 0);
    if (twelveHourTimes == false) {
      timeRaw = pad(date.getHours()) + ':' + pad(date.getMinutes());
      timeString = $(document.createElement("span")).attr("title", uitext.timezonehover + " (UTC" + (timezoneOffset < 0 ? timezoneOffset / 60 : "+" + timezoneOffset / 60) + ")").text(pad(date.getHours()) + ':' + pad(date.getMinutes()));
    } else {
      timeRaw = (((date.getHours() + 11) % 12) + 1) + ':' + pad(date.getMinutes()) + ' ' + (date.getHours() >= 12 ? 'PM' : 'AM');
      timeString = $(document.createElement("span")).attr("title", uitext.timezonehover + " (UTC" + (timezoneOffset < 0 ? timezoneOffset / 60 : "+" + timezoneOffset / 60) + ")").text((((date.getHours() + 11) % 12) + 1) + ":" + pad(date.getMinutes()) + " " + (date.getHours() >= 12 ? "PM" : "AM"));
    }
  }
  return { raw: timeRaw, string: timeString };
}

// Utility function #6: Zero pad numbers into strings of character length two.
function pad(s) {
  return (s < 10 ? '0' : '') + s;
}

// Utility function #7: Create a one-click select element for a chatlink.
function chatLinkSelect(chatLinkCode) {
  var span = document.createElement('span');
  span.innerHTML = chatLinkCode;

  var input = document.createElement('input');
  input.className = 'chatlink';
  input.type = 'text';
  input.value = chatLinkCode;
  input.readOnly = true;
  input.spellcheck = false;

  $(span).click(function () {
    this.style.visibility = 'hidden';
    input.style.display = 'inline-block';
    input.focus();
    input.select();
  });
  $(input).blur(function () {
    this.style.display = null;
    span.style.visibility = null;
  });

  var output = document.createElement('span');
  output.className = 'event-chatlink';
  output.appendChild(input);
  output.appendChild(span);
  return output;
}

// Utility function #8: Draw blocks for the given object
function drawRow(metaKey, metaSingular) {
  // Create a bar container (this will hold the bar and the associated title)
  var barcontainer = $(document.createElement("div")).attr("class", "event-bar-container " + metaKey).attr("data-abbr", metaKey);

  // Display category if not used before
  if (typeof metaSingular.category != 'undefined' && usedHeadings.indexOf(metaSingular.category) == -1) {
    usedHeadings.push(metaSingular.category);
    barcontainer.append($(document.createElement("h3")).attr("class", metaKey).text(metaSingular.category));
  }

  // Display heading with link always
  if (typeof metaSingular.link != 'undefined') {
    barcontainer.append($(document.createElement("h4"))
      .append($(document.createElement("span"))
        .append(wikiLink(metaSingular.link, metaSingular.name))
      )
    );
  } else {
    barcontainer.append($(document.createElement("h4"))
      .append($(document.createElement("span"))
        .append(wikiLink(metaSingular.name))
      )
    );
  }

  // Create a bar for the meta segments
  var bar = $(document.createElement("div")).attr("class", "event-bar");

  // For each event create a "segment"
  $.each(metaSingular.sequences.refined, function (i, v) {
    var name = metaSingular.segments[v.r].name, link = metaSingular.segments[v.r].link || (name == '' ? '' : name), chatlink = metaSingular.segments[v.r].chatlink || '';
    var time = unwrapUTC(v['s']);

    // Create a segment to represent that phase, and set the width based on the duration
    var segment = $(document.createElement("div")).attr("class", "event-bar-segment " + metaKey + v.r + v.cl).css("width", (100 * v["d"] / 135) + "%").attr("title", (metaSingular.name ? metaSingular.name + "\r" : "") + time.raw + (name == "" ? "" : " - ") + name);

    // Time
    var segmentTime = $(document.createElement("span")).attr("class", "event-time")
      .append(time.string);
    segment.append(segmentTime);

    // Segment event name and link
    // Use NBSP instead of leaving empty event names, as if the name is blank for a whole 2hr15 slot, then the segment height will reduce if the span element is empty
    if (name == "") { name = "\u00a0"; }
    segment.append($(document.createElement("span")).attr("class", "event-name")
      .append(link == "" ? document.createTextNode(name) : wikiLink(link, name)));

    // Chatlink
    if (chatlink != '') {
      segment.append(chatLinkSelect(chatlink));
    }

    bar.append(segment);
  });
  bar.append($(document.createElement("span")).attr("class", "event-bar-exit").attr("title", uitext.deleterowhover).text("[X]"));
  barcontainer.append(bar);

  $('#event-container').append(barcontainer);
}

// Utility function #9: Refine the schedule from 1515 to 135 minutes. Firstly apply a rough filter around the window, then truncate to ensure events are within the window.
function filterEventData(metas) {
  function refineRow(schedule, metaKey) {
    // Window start, future and end times in minutes
    var ws = startHourUTC * 60;
    var wf = ws + 120;
    var we = ws + 135;

    // Filter the data down from 24 hours to roughly 2 hours.
    function timeWithinWindow(schedule) {
      return ((schedule.e > ws && schedule.s < we));
    }
    var roughSchedule = schedule.filter(timeWithinWindow);

    // Refine the data to restrict lengths to visible window
    var refinedSchedule = [];
    $.each(roughSchedule, function (i, v) {
      // Local copies that we can adjust
      var r = v.r, s = v.s, e = v.e;

      // Check if window starts after the segment started, if so, crop it
      if (ws > s) {
        s = ws;
        if (metaKey == 'ds' && r == 1) {
          r = 2; // Special case: Dragon's Stand
        }
      }

      // Check end of segment is before window end, if not, crop it
      if (e > we) {
        e = we;
      }

      // Check if segment crosses the 2 hour marker, if it does, split into two
      if (s < wf && wf < e) {
        // Two objects, one beginning to the left of the future line + ending at the future line, and one starting at the future line
        refinedSchedule.push({
          r: r,            // Reference id, e.g. wb1
          s: s,            // Start minutes, e.g. 10
          e: wf,         // End minutes, e.g. 60
          d: wf - s, // Duration, e.g. 50
          cl: ''         // Class placeholder only used for future last 15 minutes segments
        });
        if (metaKey == 'ds' && r == 1) {
          r = 2; // Special case: Dragon's Stand future
        }
        refinedSchedule.push({
          r: r,
          s: wf,
          e: e,
          d: e - wf,
          cl: ' future'
        });
      } else if (wf < e) {
        // Just one object, with the ending after the future line + beginning on or after future line
        refinedSchedule.push({
          r: r,
          s: s,
          e: e,
          d: e - s,
          cl: ' future'
        });
      } else {
        // Just one object, with the ending on or before the future line
        refinedSchedule.push({
          r: r,
          s: s,
          e: e,
          d: e - s,
          cl: ''
        });
      }
    });
    return refinedSchedule;
  }

  // Refine schedule to fit 135 minute view.
  $.each(metas, function (k, v) {
    metas[k].sequences.refined = refineRow(v.sequences.full, k);
  });
  return metas;
}

// Utility function #10: Draw meta event "phases" as segments within map "bars" for each meta.
function createEventBars(useEvenHourStart, metaSequence, otherHourOffset) {
  // All event bars and segments need to be created with the same start time
  var now = new Date();
  startHourUTC = now.getUTCHours();

  // Check if otherHour specified
  if (otherHourOffset) {
    startHourUTC += otherHourOffset;
  }

  // Use even hours if required, or any hour if not specified
  if (useEvenHourStart === true) {
    startHourUTC = Math.floor(startHourUTC / 2) * 2;
  }

  // Filter the schedule for the current 135 minute window
  customEventData = filterEventData(customEventData);

  // Reset any previously set category heading tracking
  usedHeadings = [];

  // Do the work
  $.each(metaSequence, function (i, metaKey) {
    drawRow(metaKey, customEventData[metaKey]);
  });

  // Allow reordering of elements
  $('#event-container').sortable({
    placeholder: 'ui-sortable-placeholder',
    update: function () {
      // Update stored values
      var eventBars = $('.event-bar-container');
      var eventAbbrs = [];
      $.each(eventBars, function () {
        eventAbbrs.push(this.getAttribute('data-abbr'));
      });
      setEventTimerPreferences('sequence', eventAbbrs, defaultSequence);
      console.log('Rearranged sequence to: ' + JSON.stringify(eventAbbrs));

      // Now reload otherwise people whine about category titles.
      mainEventTimer(true);
    }
  });

  // Allow closure of event bars
  $('.event-bar-exit').click(function () {
    var eventBar = this.closest('.event-bar-container');
    var eventAbbr = eventBar.getAttribute('data-abbr');
    var currentPref = getEventTimerPreferences('sequence', defaultSequence);

    // Adjust stored preferences to remove given element from preferences
    var abbrIndex = currentPref.indexOf(eventAbbr);
    if (abbrIndex > -1) {
      currentPref.splice(abbrIndex, 1);
    }
    setEventTimerPreferences('sequence', currentPref, defaultSequence);
    console.log('Deleted element. Remaining sequence: ' + JSON.stringify(currentPref));

    // And hide chosen element whilst still on this page. Next time it won't load that element until you press reset.
    // $('[data-abbr="'+eventAbbr+'"]').remove(); -- not required if we redraw

    // Check if remaining sequence is only length 2 (i.e. only the the top and bottom times remain - everything else deleted)
    // If so, reset the sequence entirely.
    var revisedCurrentPref = getEventTimerPreferences('sequence', defaultSequence);
    if (revisedCurrentPref.length === 2) {
      setEventTimerPreferences('sequence', defaultSequence);
    }

    // Now reload otherwise people whine about category titles.
    mainEventTimer(true);
  });

  // fixme - no idea why, but this line is required to make everything work.
  startHourUTC = now.getUTCHours();
}

// Utility function #11: Generate a full day of meta pattern
function eventsGenerator(eventData, metaSequence) {
  function fullPatternGenerator(partial, pattern) {
    // 23:00 plus 2 hour lookahead plus 15 mins future
    var fillDuration = 60 * 25 + 15;

    // Figure out total length of partial
    var partialDuration = 0; $.map(partial, function (v) { partialDuration += v.d; });

    // If already sufficiently long, then we don't need to add any pattern sections
    var fullPattern;
    if (partialDuration >= fillDuration) {
      fullPattern = partial;
    } else {
      // Figure out total length of pattern
      var patternDuration = 0; $.map(pattern, function (v) { patternDuration += v.d; });

      // Minimum number of pattern repetitions required
      var patternQty = Math.ceil((fillDuration - partialDuration) / patternDuration);

      // Repeat pattern - can use this when we remove IE support later:
      // var repeatedPattern = Array(patternQty).fill().map(function(){ return pattern; });
      var repeatedPattern = 'z'.repeat(patternQty).split('').map(function () { return pattern; });

      // Collapse nested arrays and concatenate with the initial partial pattern
      fullPattern = partial.concat($.map(repeatedPattern, function (v) { return v; }));
    }

    // Now insert start and end markers
    var sCumulative = 0;
    fullPattern = $.map(fullPattern, function (v) {

      // Don't bother appending if cumulative start time is outside range of interest
      if (sCumulative >= fillDuration) {
        return
      }

      // Update current object
      v.s = sCumulative;
      v.e = v.s + v.d;

      // Update for next
      sCumulative = v.s + v.d;

      // Return current - note if you try to return v then it caches the result and every object returned is the same as the last one
      return {
        r: v.r,
        d: v.d,
        s: v.s,
        e: v.e
      };
    });
    return fullPattern;
  }

  var fullMetas = {};
  $.each(eventData, function (k, v) {
    // Don't bother calculating if the meta hasn't been requested
    if (metaSequence.indexOf(k) == -1) {
      return
    }
    fullMetas[k] = eventData[k];
    fullMetas[k].sequences.full = fullPatternGenerator(v.sequences.partial, v.sequences.pattern);
  });

  return fullMetas;
}

// Utility function #12: Move the pointer to a new horizontal location based on the current time.
function movePointer(useEvenHourStart, metaSequence) {
  var now = new Date();
  var hour = now.getUTCHours();
  var minute = now.getUTCMinutes();

  // Distance in percent of the 135 minute window (2 hour + 15 mins)
  var currentStartHourUTC = hour;
  var percentOfTwoHours = ((minute / 60) * 50) * (120 / 135);
  if (useEvenHourStart === true) {
    currentStartHourUTC = Math.floor(hour / 2) * 2;
    percentOfTwoHours = (((hour % 2) + (minute / 60)) * 50) * (120 / 135);
  }

  // Move the pointer
  $('.event-pointer').css('left', percentOfTwoHours + '%');

  // Check if pointer has gone beyond the 1 or 2 hour mark, it will have slid to the left, in which case we need to redraw everything else too.
  if (startHourUTC != hour) {
    // Erase existing event bars
    $('#event-container').html('');

    // Add new ones based on the new time
    createEventBars(useEvenHourStart, metaSequence);
  }

  // Update local time too
  var timezoneOffsetString = '';
  if (timezoneOffset === 0) {
    if (twelveHourTimes == false) {
      $('.event-pointer span').text(pad(hour) + ':' + pad(minute) + ' UTC');
    } else {
      $('.event-pointer span').text((((hour + 11) % 12) + 1) + ':' + pad(minute) + ' ' + (hour >= 12 ? 'PM' : 'AM'));
    }
  } else {
    // For positive timezones, add a plus sign before the hour offset. Negative timezones already have a minus sign.
    timezoneOffsetString = 'UTC' + (timezoneOffset < 0 ? timezoneOffset / 60 : '+' + timezoneOffset / 60);
    if (twelveHourTimes == false) {
      $('.event-pointer span').text(pad(now.getHours()) + ':' + pad(now.getMinutes()) + ' ' + timezoneOffsetString);
    } else {
      $('.event-pointer span').text((((now.getHours() + 11) % 12) + 1) + ':' + pad(now.getMinutes()) + ' ' + (now.getHours() >= 12 ? 'PM' : 'AM'));
    }
  }

  // Check if pointer is beyond 78% (avoid clashing between red and gray markers)
  if (percentOfTwoHours > 78) {
    $('.event-pointer-time').css('right', '0px');
  } else {
    $('.event-pointer-time').css('right', 'inherit');
  }
}

// Utility function #13: Allowing shuffling forwards and backwards
function timeshiftOnClick() {
  // Allow user to shuffle forwards and backwards by clicking on the gray markers
  $('.event-limit-text').css('cursor', 'pointer');
  $('.event-limit-text.next').prop('title', uitext.timeshiftnexthoverpause);
  $('.event-limit-text').click(function (e) {
    $('.event-pointer').css('left', '0%');
    $('.event-pointer-time').text(uitext.timeshiftresume);
    $('.event-limit-text.prev').css('display', 'inherit');
    $('.event-limit-text.prev').prop('title', uitext.timeshiftprevhover);
    $('.event-limit-text.next').prop('title', uitext.timeshiftnexthover);

    // Figure out if next or prev was clicked
    if (e.target.classList.contains('next')) {
      otherHourOffset = otherHourOffset + 2;
    } else {
      otherHourOffset = otherHourOffset + 22;
    }

    // Restrict it to +23 hours
    otherHourOffset = otherHourOffset % 24;

    // Check if its gone beyond midnight
    if (otherHourOffset + startHourUTC >= 24) {
      otherHourOffset = otherHourOffset - 24;
    }

    // Check if offset is back to zero
    if (otherHourOffset == 0) {
      mainEventTimer(true);
    } else {
      mainEventTimer(true, true);
    }
  });
  // Restart live updating after clicking on the red marker
  $('.event-pointer-time').click(function () {
    mainEventTimer(true);
  });
}

// Utility function #14: Refit compact timer to window width on resize. Only visible with the "Compact view" checkbox ticked. H4 headings 220px to left
function fitTimerToWindowWidth() {
  var w = $('#mw-content-text')[0].offsetWidth;
  $('#EventTimerCSS3').text('#event-wrapper.compact { width: ' + (w - (220 + 20)) + 'px } ');
};

// Utility function #15: Create wiki like links; inactive when on the same page as linked to.
var pageTitlePattern = /(?:(?:\/wiki\/)(.*?)(?:\?|#|$)|(?:title=)(.*?)(?:&|#|$))/;
function wikiLink(pageName, text) {
  text = text || pageName.replace(/_/g, " ");
  pageName = pageName.replace(/ /g, "_");

  var match = pageTitlePattern.exec(location.href);
  var current = match[1] || match[2];

  if (current === pageName) {
    return $(document.createElement("a")).attr("class", "mw-selflink selflink").text(text);
  } else {
    return $(document.createElement("a")).attr("href", "/wiki/" + pageName).attr("title", pageName.replace(/_/g, " ")).text(text);
  }
}

// MAIN FUNCTION
function mainEventTimer(reloaded, paused) {
  // Collect parameter options if specified
  // var zoneParameter = '<!--{$zone|default:""|escape:"javascript"}-->';
  // var excludeParameter = '<!--{$exclude|default:""|escape:"javascript"}-->';
  var excludeParameter = '';

  // Collect parameter options if specified
  var scriptNode = $('script#Widget_Event-Timer'), zoneParameter = '';
  if ( scriptNode.length > 0 ) {
    zoneParameter = scriptNode[0].getAttribute('data-zone');
  }

  // If the timer was reloaded via apply, or scrolled, reset event content and timers, otherwise its the first run and we need to create the preferences user interface.
  if (reloaded || paused) {
    $('#event-container').html('');
    $('#event-wrapper').removeClass();
    clearInterval(setIntervalHandle);
  } else if (zoneParameter == '') {
    // Display checkboxes if showing every timer (probably on the Event timers page)
    eventTimerPreferences();
  }

  // Collect preferences from localStorage
  var useTwelveHour = getEventTimerPreferences('twelvehour', uitext.checkboxes.twelvehour.defaultvalue);
  var useTopTimes = getEventTimerPreferences('toptimes', uitext.checkboxes.toptimes.defaultvalue);
  var useCompact = getEventTimerPreferences('compact', uitext.checkboxes.compact.defaultvalue);
  var hideCategories = getEventTimerPreferences('hidecategories', uitext.checkboxes.hidecategories.defaultvalue);
  var hideHeadings = getEventTimerPreferences('hideheadings', uitext.checkboxes.hideheadings.defaultvalue);
  var hideChatLinks = getEventTimerPreferences('hidechatlinks', uitext.checkboxes.hidechatlinks.defaultvalue);
  var useEvenHourStart = getEventTimerPreferences('even', uitext.checkboxes.even.defaultvalue);

  // Check for sequence preferences set by a previous version of the event timer, if so, overwrite
  var lastVersion = getEventTimerPreferences('version', '0');
  if (lastVersion != version) {
    setEventTimerPreferences('version', version);
    setEventTimerPreferences('sequence', defaultSequence);
  }

  // Respect preferences if given and the zone parameter is specified
  var metaSequence = getEventTimerPreferences('sequence', defaultSequence);
  if (zoneParameter !== '') {
    // Zone parameter is set

    // Validate zone inputs exist in the full list
    var whitelist = Object.keys(eventData);
    var zones = [];
    $.each(zoneParameter.replace(', ', ',').split(','), function (i, v) {
      if (whitelist.indexOf(v) !== -1) {
        zones.push(v);
      }
    });

    // Check if there are no valid options remaining
    if (zones.length == 0) {
      console.log('Error - No valid options provided within the zone parameter (' + zoneParameter + ')');
      return;
    }

    // Check successful, continue - overwrite metaSequence
    $('#event-wrapper').addClass('zone');
    hideCategories = true;
    useCompact = false;
    hideHeadings = false;
    useTopTimes = false;
    hideChatLinks = false;
    metaSequence = [];
    $.each(zones, function (i, v) {
      metaSequence.push(v);
    });
  } else {
    // Zone parameter is blank
    // Exclusions
    $.each(excludeParameter.replace(', ', ',').split(','), function (i, v) {
      var index = metaSequence.indexOf(v);
      if (index !== -1) {
        metaSequence.splice(index, 1);
      }
    });

    // Check if there are no valid options remaining
    if (metaSequence.length == 0) {
      console.log('Error - Exclusions resulted in no valid options being provided (' + excludeParameter + ')');
      return;
    }
  }

  // Use viewer preferences immediately where possible
  if (hideCategories === true) {
    $('#event-wrapper').addClass('hidecategories');
  }
  if (hideHeadings === true) {
    $('#event-wrapper').addClass('hideheadings');
  }
  if (useTopTimes === true) {
    $('#event-wrapper').addClass('toptimes');
  }
  if (useCompact === true) {
    $('#event-wrapper').addClass('compact');
  }
  if (hideChatLinks === true) {
    $('#event-wrapper').addClass('hidechatlinks');
  }
  if (useTwelveHour == true) {
    twelveHourTimes = true;
  } else {
    twelveHourTimes = false;
  }

  // One off tasks: Draw meta event segmented-bars, enhance them, and add a static pointer.
  if (paused) {
    createEventBars(useEvenHourStart, metaSequence, otherHourOffset);
  } else {
    $('.event-limit-text.prev').css('display', 'none');
    otherHourOffset = 0;

    customEventData = eventsGenerator(eventData, metaSequence);
    createEventBars(useEvenHourStart, metaSequence);
    movePointer(useEvenHourStart, metaSequence);

    // Recurring tasks: Move the pointer every 10 seconds. Every 2 hours, redraw the segmented bars
    setIntervalHandle = setInterval(movePointer.bind(null, useEvenHourStart, metaSequence), 10000); // bind syntax is an IE workaround
  }

  // Paranoia - recalculate compact window width if its been reloaded
  if (reloaded) {
    fitTimerToWindowWidth();
  }
}

// DEFER LOADING SCRIPT UNTIL JQUERY IS READY. WAIT 40MS BETWEEN ATTEMPTS.
function defer(method) {
  if (window.jQuery) {
    method();
  } else {
    setTimeout(function () { defer(method) }, 40);
  }
}

// INITIALISATION
defer(function () {
  writeTimerCSS();

  // Load the event timer after loading the jquery ui module
  $.ajaxSetup({ cache: true });
  $.getScript('/index.php?title=Widget:Event-Timer/jquery_ui_sortable_min.js&action=raw&ctype=text/javascript', function (data, textStatus, jqxhr) {
    // Load the main widget from above
    mainEventTimer();
    timeshiftOnClick();
  });
});
/*</nowiki>*/