Widget:Event-Timer/script.js

Aus Guild Wars 2 Wiki
Zur Navigation springen Zur Suche springen
/*<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>*/