Jump to content

MediaWiki:Gadget-owidslider.js

From Wikipedia, the free encyclopedia
This is the current revision of this page, as edited by Ragesoss (talk | contribs) at 19:23, 21 October 2025 (update to latest version from https://mdwiki.org/w/index.php?title=MediaWiki:Gadget-owidslider.js&oldid=1482850). The present address (URL) is a permanent link to this version.
(diff) ← Previous revision | Latest revision (diff) | Newer revision → (diff)
Note: After saving, you have to bypass your browser's cache to see the changes. Google Chrome, Firefox, Microsoft Edge and Safari: Hold down the ⇧ Shift key and click the Reload toolbar button. For details and instructions about other browsers, see Wikipedia:Bypass your cache.
// 2025-08-09 Forked from https://mdwiki.org/wiki/MediaWiki:Gadget-owidslider.js (Creative Commons Attribution-ShareAlike 3.0 and 4.0 International Public License)
// Script written by Hassan.m.amin for WikiProject Med Foundation based on earlier OWIDSlider script by Bawolff and Hellerhoff.
var OWID_COUNTRY_CODES = {
  Afghanistan: "AFG",
  "Åland-Islands": "ALA",
  Albania: "ALB",
  Algeria: "DZA",
  "American-Samoa": "ASM",
  Andorra: "AND",
  Angola: "AGO",
  Anguilla: "AIA",
  Antarctica: "ATA",
  "Antigua-and-Barbuda": "ATG",
  Argentina: "ARG",
  Armenia: "ARM",
  Aruba: "ABW",
  Australia: "AUS",
  Austria: "AUT",
  Azerbaijan: "AZE",
  Bahamas: "BHS",
  Bahrain: "BHR",
  Bangladesh: "BGD",
  Barbados: "BRB",
  Belarus: "BLR",
  Belgium: "BEL",
  Belize: "BLZ",
  Benin: "BEN",
  Bermuda: "BMU",
  Bhutan: "BTN",
  Bolivia: "BOL",
  "Bonaire,-Sint-Eustatius-and-Saba": "BES",
  "Bosnia-and-Herzegovina": "BIH",
  Botswana: "BWA",
  "Bouvet-Island": "BVT",
  Brazil: "BRA",
  "British-Indian-Ocean-Territory": "IOT",
  Brunei: "BRN",
  Bulgaria: "BGR",
  "Burkina-Faso": "BFA",
  Burundi: "BDI",
  Cambodia: "KHM",
  Cameroon: "CMR",
  Canada: "CAN",
  "Cape-Verde": "CPV",
  "Cayman-Islands": "CYM",
  "Central-African-Republic": "CAF",
  Chad: "TCD",
  Chile: "CHL",
  China: "CHN",
  "Christmas-Island": "CXR",
  "Cocos-(Keeling)-Islands": "CCK",
  Colombia: "COL",
  Comoros: "COM",
  Congo: "COG",
  "Democratic-Republic-of-Congo": "COD",
  "Cook-Islands": "COK",
  "Costa-Rica": "CRI",
  "Cote-d'Ivoire": "CIV",
  Croatia: "HRV",
  Cuba: "CUB",
  Curaçao: "CUW",
  Cyprus: "CYP",
  Czechia: "CZE",
  Denmark: "DNK",
  Djibouti: "DJI",
  Dominica: "DMA",
  "Dominican-Republic": "DOM",
  Ecuador: "ECU",
  Egypt: "EGY",
  "El-Salvador": "SLV",
  "Equatorial-Guinea": "GNQ",
  Eritrea: "ERI",
  Estonia: "EST",
  Ethiopia: "ETH",
  "Falkland-Islands-(Malvinas)": "FLK",
  "Faroe-Islands": "FRO",
  Fiji: "FJI",
  Finland: "FIN",
  France: "FRA",
  "French-Guiana": "GUF",
  "French-Polynesia": "PYF",
  "French-Southern-Territories": "ATF",
  Gabon: "GAB",
  Gambia: "GMB",
  Georgia: "GEO",
  Germany: "DEU",
  Ghana: "GHA",
  Gibraltar: "GIB",
  Greece: "GRC",
  Greenland: "GRL",
  Grenada: "GRD",
  Guadeloupe: "GLP",
  Guam: "GUM",
  Guatemala: "GTM",
  Guernsey: "GGY",
  Guinea: "GIN",
  "Guinea-Bissau": "GNB",
  Guyana: "GUY",
  Haiti: "HTI",
  "Heard-Island-and-McDonald-Islands": "HMD",
  "Holy-See-(Vatican-City-State)": "VAT",
  Honduras: "HND",
  "Hong-Kong": "HKG",
  Hungary: "HUN",
  Iceland: "ISL",
  India: "IND",
  Indonesia: "IDN",
  Iran: "IRN",
  Iraq: "IRQ",
  Ireland: "IRL",
  "Isle-of-Man": "IMN",
  Israel: "ISR",
  Italy: "ITA",
  Jamaica: "JAM",
  Japan: "JPN",
  Jersey: "JEY",
  Jordan: "JOR",
  Kazakhstan: "KAZ",
  Kenya: "KEN",
  Kiribati: "KIR",
  "North-Korea": "PRK",
  "South-Korea": "KOR",
  Kuwait: "KWT",
  Kyrgyzstan: "KGZ",
  Laos: "LAO",
  Latvia: "LVA",
  Lebanon: "LBN",
  Lesotho: "LSO",
  Liberia: "LBR",
  Libya: "LBY",
  Liechtenstein: "LIE",
  Lithuania: "LTU",
  Luxembourg: "LUX",
  Macao: "MAC",
  "North-Macedonia": "MKD",
  Madagascar: "MDG",
  Malawi: "MWI",
  Malaysia: "MYS",
  Maldives: "MDV",
  Mali: "MLI",
  Malta: "MLT",
  "Marshall-Islands": "MHL",
  Martinique: "MTQ",
  Mauritania: "MRT",
  Mauritius: "MUS",
  Mayotte: "MYT",
  Mexico: "MEX",
  "Micronesia-(country)": "FSM",
  Moldova: "MDA",
  Monaco: "MCO",
  Mongolia: "MNG",
  Montenegro: "MNE",
  Montserrat: "MSR",
  Morocco: "MAR",
  Mozambique: "MOZ",
  Myanmar: "MMR",
  Namibia: "NAM",
  Nauru: "NRU",
  Nepal: "NPL",
  Netherlands: "NLD",
  "New-Caledonia": "NCL",
  "New-Zealand": "NZL",
  Nicaragua: "NIC",
  Niger: "NER",
  Nigeria: "NGA",
  Niue: "NIU",
  "Norfolk-Island": "NFK",
  "Northern-Mariana-Islands": "MNP",
  Norway: "NOR",
  Oman: "OMN",
  Pakistan: "PAK",
  Palau: "PLW",
  "Palestinian-Territory,-Occupied": "PSE",
  Panama: "PAN",
  "Papua-New-Guinea": "PNG",
  Paraguay: "PRY",
  Peru: "PER",
  Philippines: "PHL",
  Pitcairn: "PCN",
  Poland: "POL",
  Portugal: "PRT",
  "Puerto-Rico": "PRI",
  Qatar: "QAT",
  Réunion: "REU",
  Romania: "ROU",
  Russia: "RUS",
  Rwanda: "RWA",
  "Saint-Barthélemy": "BLM",
  "Saint-Helena,-Ascension-and-Tristan-da-Cunha": "SHN",
  "Saint-Kitts-and-Nevis": "KNA",
  "Saint-Lucia": "LCA",
  "Saint-Martin-(French-part)": "MAF",
  "Saint-Pierre-and-Miquelon": "SPM",
  "Saint-Vincent-and-the-Grenadines": "VCT",
  Samoa: "WSM",
  "San-Marino": "SMR",
  "Sao-Tome-and-Principe": "STP",
  "Saudi-Arabia": "SAU",
  Senegal: "SEN",
  Serbia: "SRB",
  Seychelles: "SYC",
  "Sierra-Leone": "SLE",
  Singapore: "SGP",
  "Sint-Maarten-(Dutch-part)": "SXM",
  Slovakia: "SVK",
  Slovenia: "SVN",
  "Solomon-Islands": "SLB",
  Somalia: "SOM",
  "South-Africa": "ZAF",
  "South-Georgia-and-the-South-Sandwich-Islands": "SGS",
  "South-Sudan": "SSD",
  Spain: "ESP",
  "Sri-Lanka": "LKA",
  Sudan: "SDN",
  Suriname: "SUR",
  "Svalbard-and-Jan-Mayen": "SJM",
  Eswatini: "SWZ",
  Sweden: "SWE",
  Switzerland: "CHE",
  Syria: "SYR",
  "Taiwan,-Province-of-China": "TWN",
  Tajikistan: "TJK",
  Tanzania: "TZA",
  Thailand: "THA",
  "East-Timor": "TLS",
  Togo: "TGO",
  Tokelau: "TKL",
  Tonga: "TON",
  "Trinidad-and-Tobago": "TTO",
  Tunisia: "TUN",
  Turkey: "TUR",
  Turkmenistan: "TKM",
  "Turks-and-Caicos-Islands": "TCA",
  Tuvalu: "TUV",
  Uganda: "UGA",
  Ukraine: "UKR",
  "United-Arab-Emirates": "ARE",
  "United-Kingdom": "GBR",
  "United-States": "USA",
  "United-States-Minor-Outlying-Islands": "UMI",
  Uruguay: "URY",
  Uzbekistan: "UZB",
  Vanuatu: "VUT",
  Venezuela: "VEN",
  Vietnam: "VNM",
  "Virgin-Islands,-British": "VGB",
  "Virgin-Islands,-U.S.": "VIR",
  "Wallis-and-Futuna": "WLF",
  "Western-Sahara": "ESH",
  Yemen: "YEM",
  Zambia: "ZMB",
  Zimbabwe: "ZWE",
};

var OWID_WIKIDATA_COUNTRY_MAP = {
  "frenchguiana": "Q3769",
  "frenchsouthernterritories": "Q129003",
  "kosovo": "Q1246",
  "liechtenstein": "Q347",
  "newcaledonia": "Q33788",
  "westernsahara": "Q6250",
  "russia": "Q159",
  "unitedstates": "Q30",
  "canada": "Q16",
  "china": "Q148",
  "brazil": "Q155",
  "greenland": "Q223",
  "australia": "Q408",
  "fiji": "Q712",
  "india": "Q668",
  "indonesia": "Q252",
  "argentina": "Q414",
  "kazakhstan": "Q232",
  "norway": "Q20",
  "mexico": "Q96",
  "algeria": "Q262",
  "democraticrepublicofcongo": "Q974",
  "mongolia": "Q711",
  "saudiarabia": "Q851",
  "chile": "Q298",
  "iran": "Q794",
  "mali": "Q912",
  "japan": "Q17",
  "peru": "Q419",
  "pakistan": "Q843",
  "sudan": "Q1049",
  "libya": "Q1016",
  "southafrica": "Q258",
  "colombia": "Q739",
  "niger": "Q1032",
  "sweden": "Q34",
  "mozambique": "Q1029",
  "ethiopia": "Q115",
  "angola": "Q916",
  "chad": "Q657",
  "myanmar": "Q836",
  "namibia": "Q1030",
  "bolivia": "Q750",
  "mauritania": "Q1025",
  "venezuela": "Q717",
  "newzealand": "Q664",
  "uzbekistan": "Q265",
  "ukraine": "Q212",
  "somalia": "Q1045",
  "france": "Q142",
  "afghanistan": "Q889",
  "italy": "Q38",
  "papuanewguinea": "Q691",
  "philippines": "Q928",
  "thailand": "Q869",
  "turkey": "Q43",
  "tanzania": "Q924",
  "malaysia": "Q833",
  "egypt": "Q79",
  "centralafricanrepublic": "Q929",
  "nigeria": "Q1033",
  "zambia": "Q953",
  "finland": "Q33",
  "micronesia": "Q3359409",
  "vietnam": "Q881",
  "turkmenistan": "Q874",
  "southsudan": "Q958",
  "madagascar": "Q1019",
  "morocco": "Q1028",
  "spain": "Q29",
  "botswana": "Q963",
  "cameroon": "Q1009",
  "kenya": "Q114",
  "iraq": "Q796",
  "unitedkingdom": "Q145",
  "oman": "Q842",
  "germany": "Q183",
  "paraguay": "Q733",
  "yemen": "Q805",
  "congo": "Q971",
  "laos": "Q819",
  "poland": "Q36",
  "zimbabwe": "Q954",
  "belarus": "Q184",
  "greece": "Q41",
  "kyrgyzstan": "Q813",
  "romania": "Q218",
  "burkinafaso": "Q965",
  "guinea": "Q1006",
  "eritrea": "Q986",
  "cotedivoire": "Q1008",
  "ecuador": "Q736",
  "cuba": "Q241",
  "gabon": "Q1000",
  "northkorea": "Q423",
  "guyana": "Q734",
  "syria": "Q858",
  "nepal": "Q837",
  "iceland": "Q189",
  "tajikistan": "Q863",
  "uganda": "Q1036",
  "tunisia": "Q948",
  "ghana": "Q117",
  "tuvalu": "Q672",
  "bangladesh": "Q902",
  "senegal": "Q1041",
  "uruguay": "Q77",
  "solomonislands": "Q685",
  "malawi": "Q1020",
  "croatia": "Q224",
  "cambodia": "Q424",
  "azerbaijan": "Q227",
  "austria": "Q40",
  "nicaragua": "Q811",
  "honduras": "Q783",
  "hungary": "Q28",
  "bulgaria": "Q219",
  "benin": "Q962",
  "jordan": "Q810",
  "suriname": "Q730",
  "unitedarabemirates": "Q878",
  "portugal": "Q45",
  "czechia": "Q213",
  "latvia": "Q211",
  "georgia": "Q230",
  "guatemala": "Q774",
  "serbia": "Q403",
  "liberia": "Q1014",
  "southkorea": "Q884",
  "panama": "Q804",
  "ireland": "Q27",
  "lithuania": "Q37",
  "denmark": "Q35",
  "slovakia": "Q214",
  "estonia": "Q191",
  "netherlands": "Q55",
  "costarica": "Q800",
  "moldova": "Q217",
  "bosniaaherzegovin": "Q225",
  "sierraleone": "Q1044",
  "togo": "Q945",
  "switzerland": "Q39",
  "dominicanrepublic": "Q786",
  "srilanka": "Q854",
  "armenia": "Q399",
  "belgium": "Q31",
  "bahamas": "Q778",
  "capeverde": "Q1011",
  "taiwan": "Q865",
  "israel": "Q801",
  "haiti": "Q790",
  "albania": "Q222",
  "bhutan": "Q917",
  "guineabissau": "Q1007",
  "lesotho": "Q1013",
  "tonga": "Q678",
  "slovenia": "Q215",
  "burundi": "Q967",
  "northmacedonia": "Q221",
  "rwanda": "Q1037",
  "montenegro": "Q236",
  "elsalvador": "Q792",
  "djibouti": "Q977",
  "belize": "Q242",
  "kuwait": "Q817",
  "easttimor": "Q574",
  "cyprus": "Q229",
  "equatorialguinea": "Q983",
  "vanuatu": "Q686",
  "lebanon": "Q822",
  "marshallislands": "Q709",
  "eswatini": "Q1050",
  "gambia": "Q1005",
  "brunei": "Q921",
  "jamaica": "Q766",
  "palestine": "Q219060",
  "qatar": "Q846",
  "comoros": "Q970",
  "puertorico": "Q1183",
  "trinidadandtobago": "Q754",
  "samoa": "Q683",
  "luxembourg": "Q32",
  "mauritius": "Q1027",
  "singapore": "Q334",
  "antiguaandbarbuda": "Q781",
  "maldives": "Q826",
  "kiribati": "Q710",
  "saotomeandprincipe": "Q1039",
  "dominica": "Q784",
  "saintkittsandnevis": "Q763",
  "bahrain": "Q398",
  "saintlucia": "Q760",
  "andorra": "Q228",
  "barbados": "Q244",
  "seychelles": "Q1042",
  "malta": "Q233",
  "saintvincentandthegrenadines": "Q757",
  "grenada": "Q769",
  "palau": "Q695",
  "sanmarino": "Q238",
  "nauru": "Q697",
  "monaco": "Q235"
};

var OWID_WIKIDATA_COUNTRY_MAP_REVERSE = Object.fromEntries(
    Object.entries(OWID_WIKIDATA_COUNTRY_MAP).map(function([key, value]) {
    	return [value, key];
    })
)

var OWIDSlider = {
  messages: {
    en: {
      OWIDSliderFrameBack: "Back",
      OWIDSliderFrameBackDesktop: "Return to article",
      OWIDSliderFrameImageCredit: "Media credits",
      OWIDSliderFrameCopyLink: "Copy Direct Link",
      OWIDSliderSliderLabel: "Select image",
      OWIDSliderSelectRegion: "Select region",
      OWIDSliderPlayLabel: "Show slideshow",
      OWIDSliderLoading: "Loading... $1%",
    },
  },

  init: function () {
    OWIDSlider.setMessages();
    mw.hook("wikipage.content").add(OWIDSlider.addPlayButton);
  },
	
  purify: function (dirty) {
    // We use SVGs in an html context not XML, so we need to be sure they are safe.
    // This is a bit stricter than necessary, but owid graphs should have all this.
    DOMPurify.addHook( 'uponSanitizeAttribute', ( node, hook ) => {
      if ( [ 'font', 'clip-path', 'fill', 'filter', 'marker', 'marker-end', 'marker-mid', 'marker-start', 'mask', 'stroke', 'cursor' ].includes( hook.attrName ) ) {
        if (hook.attrValue.match( /url\((?!\s*['"]?#)|image-set\(|src\(/i ) ) {
          hook.keepAttr = false;
        }
      }
      if ( [ 'href', 'xlink:href' ].includes( hook.attrName ) ) {
        hook.keepAttr = false;
      }
    } );
    DOMPurify.addHook( 'uponSanitizeElement', ( node ) => {
      if ( node && node.tagName && node.tagName.toLowerCase() === 'style' && node.textContent.match( /url\((?!\s*['"]?#)|image-set\(|src\(/i ) ) {
        node.textContent = '';
      }
    } );
    var res = DOMPurify.sanitize(dirty, {USE_PROFILES: {svg: true}});
    DOMPurify.removeHook( 'uponSanitizeAttribute' );
    DOMPurify.removeHook( 'uponSanitizeElement' );
    return res;
   },
  /**
   * Set the interface messages in the most appropriate language
   *
   * Favor the user language first, the page language second, the wiki language third, and lastly English
   */
  setMessages: function () {
    var userLanguage = mw.config.get("wgUserLanguage");
    if (userLanguage in OWIDSlider.messages) {
      mw.messages.set(OWIDSlider.messages[userLanguage]);
      return;
    }
    var pageLanguage = mw.config.get("wgPageContentLanguage");
    if (pageLanguage in OWIDSlider.messages) {
      mw.messages.set(OWIDSlider.messages[pageLanguage]);
      return;
    }
    var contentLanguage = mw.config.get("wgContentLanguage");
    if (contentLanguage in OWIDSlider.messages) {
      mw.messages.set(OWIDSlider.messages[contentLanguage]);
      return;
    }
    mw.messages.set(OWIDSlider.messages.en);
  },
  parseQueryParams: function () {
	return Object.fromEntries( new URLSearchParams( location.search ) );
  },

  /**
   * Append a play button ► to every OWIDSlider div
   */
  addPlayButton: function ($content) {
    var queryParams = OWIDSlider.parseQueryParams();
    $content.find("div.OWIDSlider").each(function () {
      var $frame = $(this);
      var viewerInfo = $frame.data("owidsliderConfig");
      if (!(viewerInfo instanceof Array)) {
        return;
      }
      // match both img and span for broken files in galleries
      $frame
        .find(".mw-file-element, .lazy-image-placeholder")
        .each(function (i) {
          if (
            viewerInfo[i] instanceof Object &&
            typeof viewerInfo[i].list === "string"
          ) {
            // We used to use unicode ▶, but was rendered inconsitently between browsers, so switch to svg.
            var $play = $("<button></button>")
              .attr({
                type: "button",
                class: "OWIDSlider-play",
                title: mw.msg("OWIDSliderPlayLabel"),
                "aria-label": mw.msg("OWIDSliderPlayLabel"),
              })
              .html( '<svg viewbox="0 0 10 10"><circle class="circle" cx="5" cy="5" r="5"></circle><polygon class="triangle" points="2.5,2 8.5,5 2.5,8"></polygon></svg>' );
            var data = viewerInfo[i];
            $play.on("click", function (e) {
              e.preventDefault();
              if (
                !queryParams.owid_list ||
                queryParams.owid_list.toLowerCase() !=
                  decodeURIComponent(data.list.toLowerCase())
              ) {
              	var newUrl = new URL(window.location.href);
				newUrl.searchParams.set("owid_list", encodeURIComponent(data.list));
				if (data.language && data.language != "") {
					newUrl.searchParams.set("owid_language", encodeURIComponent(data.language));
				}
				
				window.history.pushState({}, "", newUrl);
				OWIDSlider.showFrame(data);
              } else {
                OWIDSlider.showFrame(data);
              }
            });
            var $this = $(this);
            $this.parent().css({
              display: "inline-block",
              height: "fit-content",
              position: "relative",
            });
            $this.after($play);
            var listMatch = queryParams.owid_list && data.list &&
              decodeURIComponent(queryParams.owid_list.toLowerCase()) == decodeURIComponent(data.list.toLowerCase());
            var langMatch = (
                	// Handle language cases
                	// 1. No language in uri and this doesnt have language
                	(!data.language && !queryParams.owid_language) ||
                	// 2. Language in uri and this has language
                	(data.language && queryParams.owid_language && data.language == decodeURIComponent(queryParams.owid_language.toLowerCase()))
            );
            if (
            	listMatch && langMatch
            ) {
              OWIDSlider.showFrame(data);
            }
          }
        });
    });
  },

  showFrame: function (data) {
    // Load dependencies
    var stateWindow = mw.loader.getState("oojs-ui-windows");
    var stateDomPurify = mw.loader.getState("dompurify");
    if (stateWindow !== "ready" || stateDomPurify !== "ready") {
      mw.loader.using(["oojs-ui-windows", "dompurify"], function () {
        setTimeout(function () {
          OWIDSlider.showFrame(data);
        }, 500);
      });
      return;
    }
    var $viewer = OWIDSlider.getViewer();
    var backButtonTitle = mw.msg("OWIDSliderFrameBack");
    if (window.outerWidth > 600) {
      backButtonTitle = mw.msg("OWIDSliderFrameBackDesktop");
    }

    var config = {
      size: "full",
      // This doesn't seem to work.
      classes: "OWIDSliderDialog",
      title: typeof data.title === "string" ? data.title : false,
      actions: [
        {
          action: "accept",
          label: backButtonTitle,
          flags: ["primary", "progressive"],
        },
      ],
      message: $viewer,
    };

    var dialog = function (config) {
      dialog.super.call(this, config);
      this.$element.addClass("OWIDSliderDialog");
    };

    OO.inheritClass(dialog, OO.ui.MessageDialog);
    dialog.static.name = "OWIDSlider";
    OO.ui.getWindowManager().addWindows([new dialog()]);
    // copied from OO.ui.alert definition.

    var win = OO.ui.getWindowManager().openWindow("OWIDSlider", config);
    win.closing.done(function () {
    	var newUrl = new URL(window.location.href);
		newUrl.searchParams.delete("owid_list");
		newUrl.searchParams.delete("owid_language");
				
		window.history.pushState({}, "", newUrl);
    });
    win.closed.done(function () {
      // There has to be a better way to do this.
      if (window.OWIDSliderCancel) {
        window.OWIDSliderCancel();
      }
    });
    OWIDSlider.loadImages($viewer, data);
  },

  getViewer: function () {
    var $viewer = $("<div></div>").attr({
      class: "OWIDSlider-viewer OWIDSlider-loading",
    });
    // From https://commons.wikimedia.org/wiki/File:Loading_spinner.svg
    $viewer.append(
      '<svg xmlns="http://www.w3.org/2000/svg" aria-label="Loading..." viewBox="0 0 100 100" width="25%" height="25%" style="display:block;margin:auto"><rect fill="#555" height="6" opacity=".083" rx="3" ry="3" transform="rotate(-60 50 50)" width="25" x="72" y="47"/><rect fill="#555" height="6" opacity=".167" rx="3" ry="3" transform="rotate(-30 50 50)" width="25" x="72" y="47"/><rect fill="#555" height="6" opacity=".25" rx="3" ry="3" width="25" x="72" y="47"/><rect fill="#555" height="6" opacity=".333" rx="3" ry="3" transform="rotate(30 50 50)" width="25" x="72" y="47"/><rect fill="#555" height="6" opacity=".417" rx="3" ry="3" transform="rotate(60 50 50)" width="25" x="72" y="47"/><rect fill="#555" height="6" opacity=".5" rx="3" ry="3" transform="rotate(90 50 50)" width="25" x="72" y="47"/><rect fill="#555" height="6" opacity=".583" rx="3" ry="3" transform="rotate(120 50 50)" width="25" x="72" y="47"/><rect fill="#555" height="6" opacity=".667" rx="3" ry="3" transform="rotate(150 50 50)" width="25" x="72" y="47"/><rect fill="#555" height="6" opacity=".75" rx="3" ry="3" transform="rotate(180 50 50)" width="25" x="72" y="47"/><rect fill="#555" height="6" opacity=".833" rx="3" ry="3" transform="rotate(210 50 50)" width="25" x="72" y="47"/><rect fill="#555" height="6" opacity=".917" rx="3" ry="3" transform="rotate(240 50 50)" width="25" x="72" y="47"/></svg>'
    );
    return $viewer;
  },
  loadImages: function ($viewer, data) {
    var url = "";
    var page = mw.Title.newFromText(data.list);
	if (!page) {
	   console.log("Image stack error, invalid page " + data.list);
	   return;
	}
    if (data.location && data.location.toLowerCase() == "commons") {
    	var templateName = page.title;

		var api = new mw.Api({
			userAgent: 'OWIDSlider',
		    ajax: {
		        url: 'https://commons.wikimedia.org/w/api.php'
		    }
		});		
		api.get({
		    action: 'parse',
		    page: data.list,
		    format: 'json',
		    prop: 'text',
			uselang: data.language,
		    origin: '*'  // This helps with CORS
		}).then(function(res) {
		    if (res.parse) {
		        const text = res.parse.text['*'];
				return OWIDSlider.handlePage($viewer, data, text);
		    }
		}).catch(function(error) {
		    console.error('Error:', error);
		});
    } else {
		url = page.getUrl();
		fetch(url)
	      .then(function (response) {
	        return response.text();
	      })
	      .then(function (text) {
	        return OWIDSlider.handlePage($viewer, data, text);
	      });
    }
  },

  handlePage: function ($viewer, data, text) {
    var parser = new DOMParser();
    var listDoc = parser.parseFromString(text, "text/html");
    var idSelector = mw.Title.newFromText(data.list).getFragment();

    var listElm = listDoc.getElementById(idSelector);
    if (!listElm || !listElm.dataset.owidSubids) {
      console.log("Error finding element in list document", idSelector, listElm);
      return;
    }

    var subIds = JSON.parse(listElm.dataset.owidSubids);
    if (!subIds || typeof subIds !== "object") {
      console.log("invalid owidsubids");
      return;
    }

    var years = Object.create( null ),
      imgMap = Object.create( null );
    var min = 1e9,
      max = -1e9,
      width,
      height,
      viewMin;
    var countriesUrls = Object.create( null );
    var countriesInfoUrls = Object.create( null );
    this.translatedCountryNames = Object.create(null);
    for (var galleryName in subIds) {
      var galleryId = subIds[galleryName];
      var elm = listDoc.getElementById(galleryId);
      if (!elm) {
        throw new Error("Could not find gallery with id " + galleryId);
      }
      if (galleryName == "AllCountries") {
        years[galleryName] = JSON.parse(elm.dataset.owidsliderCountry);
      } else {
        years[galleryName] = JSON.parse(elm.dataset.owidsliderYear);
      }
      if (
        !(years[galleryName] instanceof Array) ||
        years[galleryName].length < 1
      ) {
        throw new Error("Invalid data-owidslider-years " + galleryId);
      }

      var imgs = elm.querySelectorAll(
        'img.mw-file-element, span[typeof~="mw:Error"][typeof~="mw:File"]'
      );
      if (galleryName == "AllCountries") {
        for (var j = 0; j < imgs.length; j++) {
          if (imgs[j].nodeName !== "IMG") {
            continue;
          }
          countriesUrls[years[galleryName][j]] = this.convertThumbUrlToOriginal(
            imgs[j].getAttribute("src")
          );
          if (imgs[j].parentElement.href) {
        	countriesInfoUrls[years[galleryName][j]] = imgs[j].parentElement.href;
    	  }
        }
      } else {
        imgMap[galleryName] = [];
        for (var i = 0; i < imgs.length; i++) {
          if (
            typeof years[galleryName][i] != "number" ||
            imgs[i].nodeName !== "IMG"
          ) {
            continue;
          }
          imgMap[galleryName][years[galleryName][i]] = imgs[i];
          if (years[galleryName][i] < min) {
            min = years[galleryName][i];
            width = imgMap[galleryName][min].width;
            height = imgMap[galleryName][min].height;
            viewMin = galleryName;
          }
          if (years[galleryName][i] > max) {
            max = years[galleryName][i];
          }
        }
      }
    }
    if (min === 1e9) {
      throw new Error("No images for slider");
    }
    var urls = this.getImagesUrls(imgMap);
    var context = new OWIDSlider.Context(
      $viewer,
      data,
      imgMap,
      urls,
      countriesUrls,
      countriesInfoUrls,
      width,
      height,
      min,
      max,
      viewMin
    );
  },

  getSource: function (imgElm, width, height) {
    // desired dimensions
    var w = width * window.devicePixelRatio;
    var h = height * window.devicePixelRatio;
    // current candidate
    var imgW = parseInt(imgElm.width);
    var imgH = parseInt(imgElm.height);
    // img tag width/height.
    var originalW = imgW;
    var originalH = imgH;
    var src = imgElm.src;
    if (imgW >= w && imgH >= h) {
      return src;
    }
    var srcSets = imgElm.srcset.split(/\s*,\s*/);
    for (var i = 0; i < srcSets.length; i++) {
      var parts = srcSets[i].match(/^(\S+)\s+([0-9.])x\s*$/);
      if (parts && parts.length === 3) {
        var pixelRatio = parseFloat(parts[2]);
        if (
          (imgW < w && originalW * pixelRatio > imgW) ||
          (imgW > w &&
            originalW * pixelRatio - w >= 0 &&
            originalW * pixelRatio < imgW)
        ) {
          imgW = originalW * pixelRatio;
          imgH = originalH * pixelRatio;
          src = parts[1];
        }
      }
    }
    return src;
  },
  convertThumbUrlToOriginal: function (thumbUrl) {
    var urlParts = thumbUrl.split("/");
    var fileName = urlParts.filter(function (a) {
      return a && a.includes(".svg");
    })[0];
    var fileNameIndex = urlParts.indexOf(fileName);
    var hashes = urlParts.slice(fileNameIndex - 2, fileNameIndex);
    var prefix = thumbUrl.split("thumb/")[0];
    return prefix + hashes.join("/") + "/" + fileName;
  },
  getImagesUrls: function (imgs) {
    var urls = Object.create( null );
    for (var i in imgs) {
      var region = [];
      for (var j in imgs[i]) {
        region[j] = {
          url: this.convertThumbUrlToOriginal(imgs[i][j].getAttribute("src")),
        };
      }
      urls[i] = region;
    }
    return urls;
  },

  doStats: function () {
    if (window.OWIDSliderStatsAlreadyDone !== true) {
      window.OWIDSliderStatsAlreadyDone = true;
      const wiki = mw.config.get( 'wgDBname' );
      const title = mw.config.get( 'wgTitle' );
      const titlee = title.replace( / /g, '_' );
      const page = encodeURIComponent( titlee ).replace( /[^a-zA-Z0-9_]/g, '_' ); // Alphanumeric and underscore only
      const namespace = mw.config.get( 'wgNamespaceNumber' );
      mw.track( 'stats.mediawiki_gadget_OWIDSlider_wiki_total', 1, { wiki: wiki } );
      mw.track( 'stats.mediawiki_gadget_OWIDSlider_page_total', 1, { wiki: wiki, page: page, NS: namespace } );
    }
  },
  copyExecCommand: function (text) {
    var span = document.createElement("span");
    span.textContent = text;

    // Preserve consecutive spaces and newlines
    span.style.whiteSpace = "pre";
    span.style.webkitUserSelect = "auto";
    span.style.userSelect = "all";

    // Add the <span> to the page
    document.body.appendChild(span);
    var selection = window.getSelection();
    var range = window.document.createRange();
    selection.removeAllRanges();
    range.selectNode(span);
    selection.addRange(range);

    // Copy text to the clipboard
    var success = false;
    try {
      success = window.document.execCommand("copy");
    } finally {
      // Cleanup
      selection.removeAllRanges();
      window.document.body.removeChild(span);
    }

    return success;
  },

  copyText: function (text) {
    if (navigator.clipboard) {
      return navigator.clipboard
        .writeText(text)
        .then(function () {})
        .catch(function (err) {
          console.log("Error copying");
          console.log(err);
          OWIDSlider.copyExecCommand(text);
        });
    }
    OWIDSlider.copyExecCommand(text);
  },

  Context: function (
    $viewer,
    config,
    imgs,
    urls,
    countriesUrls,
    countriesInfoUrls,
    width,
    height,
    min,
    max,
    viewMin
  ) {
    OWIDSlider.doStats();
    this.svgUrls = urls;
    this.countriesSvgUrls = countriesUrls;
    this.countriesInfoUrls = countriesInfoUrls;
    this.translatedCountryNames = Object.create(null);
    this.$viewer = $viewer;
    this.loop = !!config.loop;
    this.start = typeof config.start === "number" ? config.start : 0;
    this.urls = null;
    this.infoUrls = null;
    this.imgs = imgs;
    this.min = min;
    this.max = max;
    this.viewMin = viewMin; // The view that has the min element
    this.currentView = config.startingView && imgs[config.startingView]
      ? config.startingView
      : Object.keys(imgs)[0];
    this.language = config.language ? config.language.trim() : "";
    this.total = Object.keys(imgs[this.currentView]).length;
    // for (var i in imgs) {
    //   this.total += Object.keys(imgs[i]).length;
    // }
    this.captionId =
      typeof config.caption === "string" ? config.caption : false;
    // Future TODO - make the size of image adaptive to screen size
    // Future TODO - handle images of different sizes and aspect ratios.
    this.width = config.width;
    this.height = config.height;
    if (this.width && !this.height) {
      this.height = (this.width * height) / width;
    }
    if (!this.width && this.height) {
      this.width = (this.height * width) / height;
    }
    this.imgWidth = width;
    this.imgHeight = height;
    this.currentImage =
      this.start >= this.min && this.start <= this.max ? this.start : this.max;
    this.pendingFrame = false;
    this.$loading = $("#OWIDSliderLoading");
    this.urlsLoaded = 0;
    this.pendingTouches = Object.create( null );
    this.prevImage = 0;
    this.config = config;

    this.init();
  },
};

// This part is based on Hellerhoff's https://commons.wikimedia.org/wiki/MediaWiki:Gadget-OWIDSlider.js
OWIDSlider.Context.prototype = {
  createLoader: function () {
    var $loading = $("#OWIDSliderLoading");
    if (!$loading.length) {
      $loading = $("<div></div>").attr({
        id: "OWIDSliderLoading",
        role: "status",
      });
      $(document.body).append($loading);
    }
    $loading.text(mw.msg("OWIDSliderLoading", "0"));
    return $loading;
  },
  init: function () {
    this.$loading = this.createLoader();
    var that = this;
    // Chrome scrolls much faster than firefox
    const SCROLL_SLOWDOWN = navigator.userAgent.includes("Chrome/") ? 5 : 2;
    this.pendingScrollDelta = 0;

    var containingWidth =
      this.$viewer[0].parentElement.parentElement.parentElement.clientWidth;
    var containingHeight =
      this.$viewer[0].parentElement.parentElement.parentElement.clientHeight;
    this.$viewer.empty();

    this.$slider = $("<input>", {
      type: "range",
      min: that.min,
      max: that.max,
      value: this.currentImage,
    }).on("input", function (e) {
      that.currentImage = parseInt(e.target.value);
      that.repaint();
    });
    this.$sliderYearPopup = $("<span></span>", {
      class: "OWIDSliderSliderYearPopup",
    }).css("visibility", "hidden");

    this.$sliderRangeContainer = $("<span></span>", {
      "aria-label": mw.msg("OWIDSliderSliderLabel"),
      class: "OWIDSliderSlider",
    })
      .append(this.$slider)
      .append(this.$sliderYearPopup);

    this.$sliderContainer = $("<div></div>")
      .attr({
        class: "OWIDSliderSliderContainer",
      })
      .append($('<span id="OWIDSliderBegin"></span>').text(this.min))
      .append(this.$sliderRangeContainer)
      .append($('<span id="OWIDSliderEnd"></span>').text(this.max));
    var handleTouchStart = this.handleTouchStart.bind(this);
    var handleTouchMove = this.handleTouchMove.bind(this);
    var handleTouchCancel = this.handleTouchCancel.bind(this);
    var handleTouchEnd = this.handleTouchEnd.bind(this);
    var touchElement = this.$viewer[0].parentElement.parentElement;
    var opt = { passive: true };

    // For now it seems like we don't have to cancel events. Unclear if we should
    touchElement.addEventListener("touchstart", handleTouchStart, opt);
    touchElement.addEventListener("touchmove", handleTouchMove, opt);
    touchElement.addEventListener("touchend", handleTouchEnd, opt);
    touchElement.addEventListener("touchcancel", handleTouchCancel, opt);

    // Hacky!
    window.OWIDSliderCancel = function () {
      touchElement.removeEventListener("touchstart", handleTouchStart, opt);
      touchElement.removeEventListener("touchmove", handleTouchMove, opt);
      touchElement.removeEventListener("touchend", handleTouchEnd, opt);
      touchElement.removeEventListener("touchcancel", handleTouchCancel, opt);
      $(".OWIDSliderDialog").html("");
    };

    var $svgContainer = $('<div class="OWIDSliderSVGContainer"></div>');
    this.$svgContainer = $svgContainer;

    $svgContainer.on("mousewheel", function (event, delta) {
      // Scroll is too fast (Esp. on chrome), so we buffer scroll events.
      that.pendingScrollDelta += delta;
      var realDelta = Math.floor(that.pendingScrollDelta / SCROLL_SLOWDOWN);
      if (delta !== 0) {
        // We reverse the direction of scroll.
        that.currentImage -= realDelta > 2 ? 2 : realDelta;
        that.pendingScrollDelta -= realDelta * SCROLL_SLOWDOWN;
        that.repaint();
      }
      return false;
    });
    $svgContainer.on("mousedown", function (event) {
      // prepare scroll by drag
      mouse_y = event.screenY; // remember mouse-position
      that.scrollobject = true; // set flag
      return false;
    });
    $svgContainer.on("mouseup", function (event) {
      that.scrollobject = false; // set flag
      return false;
    });
    $svgContainer.on("mousemove", function (event) {
      if (that.scrollobject && Math.abs(mouse_y - event.screenY) > 10) {
        var offset = mouse_y < event.screenY ? 1 : -1;
        mouse_y = event.screenY; //  remember mouse-position for next event
        that.currentImage += offset;
        that.repaint();
      }
      return false;
    });

    this.$credit = $("<a></a>");
    this.$credit.text(mw.msg("OWIDSliderFrameImageCredit"));
    var $creditDiv = $('<div class="OWIDSliderCredit"></div>')
      .append(this.$copyLink)
      .append(this.$credit);

    var $select = "";
    if (Object.keys(this.imgs).length >= 0) {
      $select = $("<select>")
        .attr("id", "OWIDSliderViewSelector")
        .attr("class", "owid-select");
      for (var i in this.imgs) {
        var optionName = i;
        if (optionName == "NorthAmerica") {
          optionName = "North America";
        } else if (optionName == "SouthAmerica") {
          optionName = "South America";
        }
        $select.append(
          $("<option>")
            .attr({ value: i, selected: this.currentView === i })
            .text(optionName)
        );
      }
      $select.change(function (e) {
        that.currentView = e.target.value;
        that.total = Object.keys(that.imgs[that.currentView]).length;
        that.urlsLoaded = 0;
        that.$loading = that.createLoader();
        that.preload();
      });
      var selectContainer = $("<div>").attr("class", "owid-select-container");
      var selectArrow = $("<span>").attr("class", "owid-select-arrow");
      var selectLabel = $("<label>")
        .attr("class", "owid-select-label")
        .text(mw.msg( 'OWIDSliderSelectRegion' ));
      selectContainer.append($select).append(selectArrow).append(selectLabel);
      $select = selectContainer;
    }

    var $container = $('<div class="OWIDSliderImgContainer"></div>')
      .append($select)
      .append($svgContainer)
      .append($creditDiv)
      .append(this.$sliderContainer);
    this.$countrySelect = $select;
    this.$container = $container;
    this.$svgContainer = $svgContainer;

    this.$viewer.append($container);
    var $wrapper = false;
    if (this.captionId) {
      var captionElm = document.getElementById(this.captionId);
      if (captionElm && captionElm.innerText !== '') {
        var newCaption = $(captionElm).clone();
        newCaption.show();
        $wrapper = $('<div class="OWIDSlider-caption"></div>').append(
          newCaption
        );
        this.$container.append($wrapper);
      }
    }

    this.cachedSvgs = Object.create( null );
    this.cachedCountriesSvgs = Object.create( null );

    this.getUrls();
    //this.toggleImg();
    this.preload();
    if (this.language && this.language != "en") {
        this.populateTranslatedCountriesNames();	
    }
    this.$slider.focus();
  },

  getMaxImgDim: function () {
    // This assumes that even on high-DPI displays, enlarging to 96dpi is ok.
    var w = this.imgs[this.viewMin][this.min].width;
    var h = this.imgs[this.viewMin][this.min].height;
    if (this.imgs[this.viewMin][this.min].srcset.match(/\s2x\s*(,|$)/)) {
      w *= 2;
      h *= 2;
    } else if (
      this.imgs[this.viewMin][this.min].srcset.match(/\s1.5x\s*(,|$)/)
    ) {
      w = Math.floor(1.5 * w);
      h = Math.floor(1.5 * h);
    }
    return [w, h];
  },

  repaint: function () {
    if (this.pendingFrame) {
      return;
    }
    requestAnimationFrame(this.toggleImg.bind(this));
  },

  toggleImg: function () {
    this.pendingFrame = true;
    if (this.prevImage < this.currentImage) {
      while (
        this.currentImage < this.max &&
        !this.imgs[this.currentView][this.currentImage]
      ) {
        this.currentImage++;
      }
      // If we get to the end and its still not valid
      while (
        this.currentImage > this.min &&
        !this.imgs[this.currentView][this.currentImage]
      ) {
        this.currentImage--;
      }
    } else {
      while (
        this.currentImage > this.min &&
        !this.imgs[this.currentView][this.currentImage]
      ) {
        this.currentImage--;
      }
      while (
        this.currentImage < this.max &&
        !this.imgs[this.currentView][this.currentImage]
      ) {
        this.currentImage++;
      }
    }
    if (this.loop) {
      if (this.currentImage < this.min) {
        this.currentImage = this.max;
      } else if (this.currentImage > this.max) {
        this.currentImage = this.min;
      }
    } else {
      if (this.currentImage < this.min) {
        this.currentImage = this.min;
      } else if (this.currentImage > this.max) {
        this.currentImage = this.max;
      }
    }
    this.prevImage = this.currentImage;
    this.$slider[0].value = this.currentImage;
    this.$slider[0].title = this.currentImage;
    this.$credit[0].href = this.infoUrls[this.currentView][this.currentImage];
    if (this.infoUrls[this.currentView][this.currentImage] === false) {
      this.$credit.css("visibility", "hidden");
    } else {
      this.$credit.css("visibility", "visible");
    }
    var currentUrl = this.svgUrls[this.currentView][this.currentImage].url;
    var that = this;

    if (this.sliderYearPopupTimeout) {
      clearInterval(this.sliderYearPopupTimeout);
    }
    this.$sliderYearPopup.text(this.currentImage);
    var popupWidth = this.$sliderYearPopup.width() || 35;
    if (popupWidth > 0) {
      var popupLeft =
        ((this.currentImage - this.min) / (this.max - this.min)) * 100;
      var calculatedPopupLeft =
        "calc(" + popupLeft + "% " + "- " + (popupWidth / 2 + 10) + "px)";
      this.$sliderYearPopup
        .css("visibility", "visible")
        .css("left", calculatedPopupLeft);
      this.sliderYearPopupTimeout = setTimeout(function () {
        that.$sliderYearPopup.css("visibility", "hidden");
        clearTimeout(that.sliderYearPopupTimeout);
      }, 5 * 1000);
    }

    this.setCurrentSvgImage(currentUrl, function () {
      that.pendingFrame = false;
    });
  },
  setSvg: function ( $svgEl ) {
    if ( $svgEl[0] instanceof SVGSVGElement ) {
      // This helps flexbox display properly.
      var width = $svgEl[0].viewBox.baseVal.width;
      var height = $svgEl[0].viewBox.baseVal.height;
      if ( width && height && width > 0 && height > 0 ) {
        this.$svgContainer.css( 'aspect-ratio', width + ' / ' + height );
      }
    }
    this.$svgContainer.html($svgEl);
  },
  setCurrentSvgImage: function (svgUrl, callback) {
  	// Check for the new flow
  	if (this.svgYears && this.svgYears[this.currentView] && this.svgYears[this.currentView][this.currentImage] && this.firstSVGData) {
  		var svgEl = $(this.firstSVGData);
  		var countriesWithData = svgEl.find("#countries-with-data path,#countries-without-data path");
  		for (var i = 0; i<countriesWithData.length; i++) {
  			var fill = this.svgYears[this.currentView][this.currentImage][countriesWithData[i].getAttribute("id")];
  			if (fill) {
  				countriesWithData[i].setAttribute("fill", fill);
  			}
  		}

  		var urlEl = svgEl.find("#header > a");
  		if (urlEl.length && urlEl.attr("href")) {
  			urlEl.attr("href", urlEl.attr("href").replace(this.min, this.currentImage));
  		}

  		var titleEl = svgEl.find("#header > a text tspan");
  		if (titleEl) {
  			titleEl.text(titleEl.text().replace(this.min, this.currentImage));
  		}

  		svgEl = this.getScaledSvg(svgEl);
		this.setSvg( svgEl );
  		this.attachSVGHandlers();
    	callback();
  		return;
  	}

    if (this.cachedSvgs[svgUrl]) {
      var svgEl = this.getScaledSvg(this.cachedSvgs[svgUrl]);
	  this.setSvg( svgEl );
      this.attachSVGHandlers();
      callback();
      return;
    }
    var that = this;
    fetch(svgUrl)
      .then(function (resp) {
        return resp.text();
      })
      .then(function (svgData) {
        // We use SVG in an HTML not XML content, so we need to resanitize
        svgData = OWIDSlider.purify( svgData );
        that.cachedSvgs[svgUrl] = svgData;
        var svgEl = that.getScaledSvg(svgData);
		this.setSvg( svgEl );
        that.attachSVGHandlers();
        callback();
      })
      .catch(function (err) {
        console.log("Error loading image", err);
      });
  },
  getScaledSvg: function (content) {
    var svgEl = $(content);
    // If the svg content has comment, it's treated as a separate node
    // So we need to extract just the svg
    if (svgEl.length > 1) {
    	for (var i=0; i<svgEl.length; i++) {
    		if (svgEl[i].tagName == "svg") {
    			svgEl = $(svgEl[i]);
    			break;
    		}
    	}
    }
	
    svgEl.removeAttr("width");
    svgEl.removeAttr("height");
    var windowWidth = window.outerWidth;
    var isMobile = windowWidth < 600;
    // if (isMobile) {
    // 	svgEl.attr("width", "100%");
    // } else {
    // 	svgEl.attr("height", "70vh");	
    // }
    svgEl.css("max-width", "100%").css("max-height", "70vh");
    // Update the viewBox
    var viewBox = svgEl.attr("viewBox");
    if (viewBox) {
    	var parts = viewBox.trim().split(" ");
		// FIXME, is this just hardcoding the normal size of the OWID files? That seems very fragile.
    	parts[3] = "550";
    	svgEl.attr("viewBox", parts.join(" "));
    }
    this.attachDetailsToInfoIcon(svgEl);
    return svgEl;
  },
  getInfoIcon: function () {
    // Original at https://commons.wikimedia.org/wiki/File:Information_icon.svg
    var $icon = $(
      '<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 62 62" width="25" height="25" version="1.0"><defs><linearGradient id="fieldGradient" gradientUnits="userSpaceOnUse" x1="42.9863" y1="7.01270" x2="22.0144" y2="51.9871"><stop offset="0.0" stop-color="#BCD6FE" /><stop offset="1.0" stop-color="#6787D3" /></linearGradient><linearGradient id="edgeGradient" gradientUnits="userSpaceOnUse" x1="55.4541" y1="42.7529" x2="9.54710" y2="16.2485"><stop offset="0.0" stop-color="#3057A7" /><stop offset="1.0" stop-color="#5A7AC6" /></linearGradient><radialGradient id="shadowGradient"><stop offset="0.0" stop-color="#C0C0C0" /><stop offset="0.88" stop-color="#C0C0C0" /><stop offset="1.0" stop-color="#C0C0C0" stop-opacity="0.0" /></radialGradient></defs><circle id="shadow" r="26.5" cy="29.5" cx="32.5" fill="url(#shadowGradient)" transform="matrix(1.0648,0.0,0.0,1.064822,-2.1,1.0864)" /><circle id="field" r="25.8" cx="31" cy="31" fill="url(#fieldGradient)" stroke="url(#edgeGradient)" stroke-width="2" /><g id="info" fill="white"><polygon points="23,25 35,25 35,44 39,44 39,48 23,48 23,44 27,44 27,28 23,28 23,25" /><circle r="4" cx="31" cy="17" /></g></svg>'
    );
    if (window.outerWidth < 600) {
    	$icon.attr("width", "50px");
    	$icon.attr("height", "50px");
    }
    return $icon;
  },
  attachDetailsToInfoIcon: function (svgEl) {
    var footer = svgEl.find("#footer");
    var separator = svgEl.find("#separator-line");
    var details = svgEl.find("#details");
    // Remove unneeded elements
    if (separator.length) {
      separator.remove();
    }
    if (details.length) {
      details.remove();
    }
    if (footer.length) {
    	footer.remove();
    }
    var detailsArr = [];
	if (this.worldDetails) {
    	detailsArr = this.parseSVGDetails($(this.worldDetails));
	} else {
		detailsArr = this.parseSVGDetails(details);
	}
    var header = svgEl.find("#header");
    var isMobile = window.outerWidth < 600;
    if (header.length && detailsArr) {
      header = header.first();
      var that = this;
      var infoIcon = this.getInfoIcon();
      svgEl.find("#logo").empty().append(infoIcon);
      infoIcon.css( 'cursor', 'pointer' );
      infoIcon.on("mouseleave", function (e) {
        $( '#details-popup.owid-details-hover' ).remove();
      } );
      infoIcon.on("click" + ( !isMobile ? " mouseenter" : "" ), function (e) {
		// Close old popup if it is currently open.
		var oldPopup = $( '#details-popup' );
		// If its a hover don't close old popup or open a new one if popup already exists.
		if ( oldPopup.length && e.type === 'mouseenter' ) {
			return
		}
		oldPopup.remove();
        var popup = $("<div>")
          .css("position", "absolute")
          .css("background-color", "white")
          .css("color", "black")
          .css("border", "1px solid #eee")
          .css("border-radius", "2px")
          .css("padding", "5px")
          .css("max-width", "400px")
          .attr("id", "details-popup")
          .attr("class", e.type === 'mouseenter' ? 'owid-details-hover' : 'owid-details-click' );
        if ( e.type === 'click' ) {
          var close = $("<button>").text("X").css( 'cursor', 'pointer' ).on("click", function() {
       		  $("#details-popup").remove();
       	  });
       	  var closeContainer = $("<div>").css("text-align", "right").append(close);
       	  popup.append(closeContainer);
        }
        if (isMobile) {
        	popup.css("top", "0px").css("right", "25px").css("max-width", "350px");
        } else {
        	popup.css("top", parseInt(e.clientY) + 15 + "px").css("left", parseInt(e.clientX) - 400 + "px");
        }
        detailsArr.forEach(function (item) {
          var itemContainer = $("<div>").css("font-size", "12px");
          var title = $("<strong>").text(item.id + ". " + item.title + ": ");
          var description = $("<span>").text(item.description);
          itemContainer.append(title).append(description);
          popup.append(itemContainer);
        });
        $(".OWIDSlider-viewer").append(popup);
      });
    }
  },
parseSVGDetails: function (svgDetails) {
  if (svgDetails.length > 0) {
    var items = svgDetails.children();
    var details = [];
    items.each(function () {
      // Get all tspan elements in this text group
      const tspans = $(this).find("tspan");

      // Extract the first line which contains the number and title
      const firstLine = tspans.first().text();
      
      // Try to match pattern with colon first: "1. Title: Description"
      var titleMatch = firstLine.match(/^(\d+)\.\s+([^:]+):\s*(.*)/);
      var number, title, firstLineContent;
      if (titleMatch) {
        // Pattern with colon found
        number = parseInt(titleMatch[1]);
        title = titleMatch[2].trim();
        firstLineContent = titleMatch[3].trim();
      } else {
        // Try pattern without colon: "1. Title Description"
        titleMatch = firstLine.match(/^(\d+)\.\s+(.+)/);
        if (titleMatch) {
          number = parseInt(titleMatch[1]);
          // For titles without colon, we need to extract the title from the styled spans
          const titleSpan = tspans.first().find('tspan[style*="font-weight:700"]').last();
          if (titleSpan.length > 0) {
            title = titleSpan.text().trim();
            // Get content after the title
            const titleEndIndex = firstLine.indexOf(title) + title.length;
            firstLineContent = firstLine.substring(titleEndIndex).trim();
          } else {
            // Fallback: assume first few words are the title
            const words = titleMatch[2].trim().split(' ');
            title = words.slice(0, 2).join(' '); // Take first 2 words as title
            firstLineContent = words.slice(2).join(' '); // Rest as description
          }
        }
      }
      
      if (number && title) {
        // Get additional content from subsequent tspans (exclude the repeated title/number)
        var additionalContent = "";
        tspans.slice(1).each(function () {
          // Exclude content that starts with the number and title again
          const text = $(this).text();
          if (!text.match(/^\d+\.\s+/)) {
            additionalContent += " " + text.trim();
          }
        });
        
        // Combine for full description
        const fullDescription = (firstLineContent + additionalContent).trim();
        
        // Add to JSON structure
        details.push({
          id: number,
          title: title,
          description: fullDescription,
        });
      }
    });
  }
  return details;
},
  attachSVGHandlers: function () {
    this.initSVGControls();
  },
  replaceTranslationData: function(svgDoc) {
	if (this.worldTitle) {
    	var currentTitle = svgDoc.querySelector("#header a text tspan");
    	if (currentTitle) {
    		currentTitle.textContent = this.worldTitle;
    	}
    }
    if (this.worldSubtitle) {
    	var currentSubtitle = svgDoc.querySelector("#header #subtitle text tspan");
    	if (currentSubtitle) {
    		currentSubtitle.textContent = this.worldSubtitle;
    	}
    }
    			
    if (this.worldDetails) {
    	var currentDetails = svgDoc.querySelector("#details");
    	if (currentDetails) {
    		currentDetails.replaceWith(this.worldDetails);
    	}
    }
    if (this.worldLabels) {
    	var currentLabels = svgDoc.querySelector("#labels");
    	if (currentLabels) {
    		currentLabels.replaceWith(this.worldLabels);
    	}
    }

    return svgDoc;
  },
  preload: function () {
    var that = this;
    var funcArray = [];
    if (!this.svgYears) {
    	this.svgYears = Object.create( null );
    }

    var svgs = [];
    var viewSvgs = this.svgUrls[this.currentView];
    var firstSvg = viewSvgs[this.min];
    fetch(firstSvg.url)
    .then(function(res) {
    	return res.text();
    })
    .then(function(svgData) {
    	// We need to extract the years first, cause DOMPurify removes the years metadata
    	var parser = new DOMParser();
    	var svgDoc = parser.parseFromString(svgData, "image/svg+xml");
    	var years = svgDoc.querySelectorAll("metadata years year");

		// Then let's purify
		svgData = OWIDSlider.purify(svgData);
		svgData = svgData.replaceAll("&nbsp;", "");
		
		parser = new DOMParser();
    	svgDoc = parser.parseFromString(svgData, "image/svg+xml");
    	if (years.length > 0) {
    		var yearsObj = Object.create( null );
    		years.forEach(function(yearEl) {
    			var year = parseInt(yearEl.getAttribute("value"));
    			yearsObj[year] = Object.create( null );
    			for (var i=0; i < yearEl.children.length; i++) {
    				var countryEl = yearEl.children[i];
    				yearsObj[year][countryEl.getAttribute("name").replace(/\s/g, "-")] = countryEl.getAttribute("fill");
    			}
    		});
    		that.firstSVGData = svgData;

    		// Remove language switches
    		var switches = svgDoc.querySelectorAll("switch");
    		switches.forEach(function (sw){
    			var enOption = null;
    			sw.querySelectorAll("text").forEach(function(text) {
    				var sysLang = text.getAttribute("systemLanguage");
    				if (!sysLang || sysLang == "en") {
    					enOption = text;
    				}
    				if (that.language && that.language != "en") {
    					if (sysLang != that.language) {
    						sw.removeChild(text);
    					} else {
    						text.removeAttribute("systemLanguage");
    					}
    				} else {
    					// It's English
    					if (sysLang && sysLang != "en") {
    						sw.removeChild(text);
    					}
    				}
   				});
   				// Fallback to English if no translation to target language is found
   				if (sw.children.length == 0 && enOption) {
   					sw.appendChild(enOption);
   				}
   			});
   			   			
   			// preserve the translations from the World map
    		if (that.currentView.toLowerCase() == "world" || (switches.length > 0 && !that.worldTitle)) {
				that.worldTitle = svgDoc.querySelector("#header a text tspan").textContent;
    			that.worldSubtitle = svgDoc.querySelector("#header #subtitle text tspan").textContent;
     			that.worldDetails = svgDoc.querySelector("#details");
    			that.worldLabels = svgDoc.querySelector("#labels");
    		} else {
    			// Check if we have world translations. If so, substitute in the svgDoc
    			svgDoc = that.replaceTranslationData(svgDoc);
    		}

    		// Convert the modified DOM back to a string
    		var serializer = new XMLSerializer();
    		that.firstSVGData = serializer.serializeToString(svgDoc);
    		that.svgYears[that.currentView] = yearsObj;
    		that.toggleImg();
    		that.removeLoadingState();
    	} else {
    		// TODO: Ask user to import or fallback to prev flow
    		for (var i = that.min; i <= that.max; i++) {
		      var svgUrl = that.svgUrls[that.currentView][i];
		      if (svgUrl) {
		        svgUrl = svgUrl.url;
		      } else {
		        continue;
		      }
		      // Previousely cached
		      if (that.cachedSvgs[svgUrl]) {
		        continue;
		      }
		      svgs.push(svgUrl);
	    	}
			that.processArray(svgs, that.loadAndCacheSvgs.bind(that));
			that.toggleImg();
    	}
    });

  },
  loadAndCacheSvgs: function (svgUrl) {
    var that = this;
    return new Promise(function (resolve) {
      fetch(svgUrl)
        .then(function (res) {
          return res.text();
        })
        .then(function (svgData) {
          svgData = OWIDSlider.purify( svgData );
          that.cachedSvgs[svgUrl] = svgData;
          that.onUrlLoaded();
          resolve(true);
        })
        .catch(function (err) {
          that.onUrlLoaded();
          console.log("Error loading svg data", svgUrl, gallery, i, err);
          resolve(false);
        });
    });
  },
  processArray: function (arr, fn) {
    var chunks = [];

    var chunk_size = 2;
    var accumilator = [];
    for (var i = 0; i < arr.length; i++) {
      if (accumilator.length >= chunk_size) {
        chunks.push(accumilator);
        accumilator = [];
      }
      accumilator.push(arr[i]);
    }
    if (accumilator.length > 0) {
      chunks.push(accumilator);
      accumilator = [];
    }

    return chunks.reduce(function (p, v) {
      return p.then(function (a) {
        return new Promise(function (resolve) {
          if (!v || v.length == 0) {
            return resolve(p);
          }
          var funcArray = [];
          for (var i = 0; i < v.length; i++) {
            funcArray.push(
              (function (url) {
                return fn(url).then(function (r) {
                  return a.concat([r]);
                });
              })(v[i])
            );
          }
          return Promise.all(funcArray).then(function () {
            resolve(a);
          });
        });
      });
    }, Promise.resolve([]));
  },

  getUrls: function () {
    this.urls = Object.create( null );
    this.infoUrls = Object.create( null );
    for (var gallery in this.imgs) {
      this.urls[gallery] = [];
      this.infoUrls[gallery] = [];
      for (var i = this.min; i <= this.max; i++) {
        if (!this.imgs[gallery][i]) {
          continue;
        }
        this.urls[gallery][i] = OWIDSlider.getSource(
          this.imgs[gallery][i],
          this.width,
          this.height
        );
        if (this.imgs[gallery][i].parentElement.href) {
          this.infoUrls[gallery][i] = this.imgs[gallery][i].parentElement.href;
        } else {
          this.infoUrls[gallery][i] = false;
        }
      }
    }
  },

  onUrlLoaded: function () {
    // For now, this still increments for failed loads, so
    // as not to have the progress bar stuck.
    this.urlsLoaded++;
    var progress = Math.floor((this.urlsLoaded / this.total) * 100);
    if (this.$loading.length) {
      this.$loading.text(mw.msg("OWIDSliderLoading", progress));
      if (this.urlsLoaded === this.total) {
        this.removeLoadingState();
      }
    }
  },
  
  removeLoadingState: function() {
  	this.urlsLoaded = this.total;
  	this.$viewer.removeClass("OWIDSlider-loading");
    this.$loading.remove();
  },

  handleTouchStart: function (e) {
    for (var i = 0; i < e.changedTouches.length; i++) {
      var t = e.changedTouches[i];
      this.pendingTouches[t.identifier] = [t.clientX, t.clientY];
    }
  },
  handleTouchCancel: function (e) {
    for (var i = 0; i < e.changedTouches.length; i++) {
      var t = e.changedTouches[i];
      delete this.pendingTouches[t.identifier];
    }
  },
  handleTouchMove: function (e) {
    for (var i = 0; i < e.changedTouches.length; i++) {
      var t = e.changedTouches[i];
      if (!this.pendingTouches[t.identifier]) {
        continue;
      }
      var startX = this.pendingTouches[t.identifier][0];
      var startY = this.pendingTouches[t.identifier][1];
      var angle = Math.abs(
        Math.atan((startY - t.clientY) / (startX - t.clientX))
      );

      if (angle > 1) {
        // vertical. > ~60 degrees
        if (Math.abs(startY - t.clientY) < 15) {
          // Not large enough
          continue;
        }
        // reset calculation so we move image if they move 15 more pixels
        this.pendingTouches[t.identifier] = [t.clientX, t.clientY];
        /* Do not do anything for vertical swipes.
              if ( startY - t.clientY > 0 ) {
                // swipe up
                this.currentImage--;
                this.repaint();
              } else {
                // swipe down.
                this.currentImage++;
                this.repaint();
              }
              */
      }
    }
  },
  handleTouchEnd: function (e) {
    for (var i = 0; i < e.changedTouches.length; i++) {
      var t = e.changedTouches[i];
      if (!this.pendingTouches[t.identifier]) {
        continue;
      }
      var startX = this.pendingTouches[t.identifier][0];
      var startY = this.pendingTouches[t.identifier][1];
      var angle = Math.abs(
        Math.atan((startY - t.clientY) / (startX - t.clientX))
      );
      if (angle < 0.7) {
        // horizontal swipe. < 40 degrees
        if (Math.abs(startX - t.clientX) < 30) {
          // Not large enough
          continue;
        }

        if (startX - t.clientX < 0) {
          // swipe left
          this.currentImage--;
          this.repaint();
        } else {
          // swipe right
          this.currentImage++;
          this.repaint();
        }
      }
      /** do not do anything for vertical swipes
            if ( angle > 1 ) {
              // vertical swipe. > ~60 degrees
              if ( Math.abs( startY - t.clientY ) < 30 ) {
                // Not large enough
                continue;
              }
              if ( startY - t.clientY > 0 ) {
                // swipe up
                this.currentImage--;
                this.repaint();
              } else {
                // swipe down
                this.currentImage++;
                this.repaint();
              }
            } */

      delete this.pendingTouches[t.identifier];
    }
  },

  initSVGControls: function (countriesUrls) {
    this.HIGHLIGHTED_STROKE_WIDTH = 1;
    this.DEFAULT_STROKE_WIDTH = 0.3;
    this.CONTAINER_SELECTOR = ".OWIDSliderSVGContainer";
    this.MAP_SELECTOR = "#chart-area"
    this.$svgContainer = $(this.CONTAINER_SELECTOR);
    this.strokeWidth = Object.create( null );
    this.originalContainerContent = this.$svgContainer.html();
    // Enable pointer events on svg content
    this.chartArea = document.querySelector(
      this.CONTAINER_SELECTOR + " #chart-area"
    );
    this.chartArea.style.pointerEvents = "all";
    this.chartArea.style.zIndex = 1;
    this.$loader = this.getLoader();
    var svgBackgroundFill = $(this.CONTAINER_SELECTOR + " .background-fill");
    if (svgBackgroundFill) {
      svgBackgroundFill.css("z-index", -1);
    }
    var $countryBack = $(".OWIDSlider-country-back-container");
    if ($countryBack.length) {
      $countryBack.remove();
    }
    this.attacHoverEventOnCountries();
    this.attachFocusCountriesOnLegendHover();
    this.attachClickEventOnCountries();
    // attachBackButton();
  },

  getLoader: function () {
    var $loader = $("<div></div>").attr({
      class: "OWIDSlider-viewer OWIDSlider-loading",
    });
    // From https://commons.wikimedia.org/wiki/File:Loading_spinner.svg
    $loader.append(
      '<svg xmlns="http://www.w3.org/2000/svg" aria-label="Loading..." viewBox="0 0 100 100" width="25%" height="25%" style="display:block;margin:auto"><rect fill="#555" height="6" opacity=".083" rx="3" ry="3" transform="rotate(-60 50 50)" width="25" x="72" y="47"/><rect fill="#555" height="6" opacity=".167" rx="3" ry="3" transform="rotate(-30 50 50)" width="25" x="72" y="47"/><rect fill="#555" height="6" opacity=".25" rx="3" ry="3" width="25" x="72" y="47"/><rect fill="#555" height="6" opacity=".333" rx="3" ry="3" transform="rotate(30 50 50)" width="25" x="72" y="47"/><rect fill="#555" height="6" opacity=".417" rx="3" ry="3" transform="rotate(60 50 50)" width="25" x="72" y="47"/><rect fill="#555" height="6" opacity=".5" rx="3" ry="3" transform="rotate(90 50 50)" width="25" x="72" y="47"/><rect fill="#555" height="6" opacity=".583" rx="3" ry="3" transform="rotate(120 50 50)" width="25" x="72" y="47"/><rect fill="#555" height="6" opacity=".667" rx="3" ry="3" transform="rotate(150 50 50)" width="25" x="72" y="47"/><rect fill="#555" height="6" opacity=".75" rx="3" ry="3" transform="rotate(180 50 50)" width="25" x="72" y="47"/><rect fill="#555" height="6" opacity=".833" rx="3" ry="3" transform="rotate(210 50 50)" width="25" x="72" y="47"/><rect fill="#555" height="6" opacity=".917" rx="3" ry="3" transform="rotate(240 50 50)" width="25" x="72" y="47"/></svg>'
    );
    return $loader;
  },

  attacHoverEventOnCountries: function () {
    // attach hover event on all countries
    var allCountries = document.querySelectorAll(
      this.CONTAINER_SELECTOR +
        " g#countries-with-data path,g#countries-without-data path"
    );

    for (var i = 0; i < allCountries.length; i++) {
      allCountries[i].onmouseenter = this.onCountryHover.bind(this);
      allCountries[i].onmouseleave = this.onCountryHoverLeave.bind(this);
      allCountries[i].style.cursor = 'pointer';
    }
  },
  getOverlayHanlderId: function (id) {
    return "overlay-" + id;
  },
  onCountryHover: function (e) {
    function createCountryPopup(config) {
      var countryPopup = document.createElement("div");
      countryPopup.style.position = "absolute";
      countryPopup.style.left = config.left;
      countryPopup.style.top = config.top;
      countryPopup.style.backgroundColor = "white";
      countryPopup.style.color = "black";
      countryPopup.style.border = "1px solid #eee";
      countryPopup.style.borderRadius = "2px";
      countryPopup.style.boxShadow = "2px 2px 2px 1px #888888";
      countryPopup.style.padding = "5px";
      var span = document.createElement("span");
	  span.textContent = config.name;
	  countryPopup.appendChild(span);
      countryPopup.id = config.id;
      return countryPopup;
    }

    var id = e.target.getAttribute("id");
    this.strokeWidth[id] = e.target.getAttribute("stroke-width");
    e.target.setAttribute("stroke-width", this.HIGHLIGHTED_STROKE_WIDTH);
	var name = id;
    var formattedName =  name.toLowerCase().replace(/[^a-z0-9]/g, '');
	if (this.translatedCountryNames[formattedName]) {
        name = this.translatedCountryNames[formattedName];
    }
    var countryPopup = createCountryPopup({
      id: this.getOverlayHanlderId(id),
      name: name,
      left: parseInt(e.clientX) + 15 + "px",
      top: parseInt(e.clientY) + 15 + "px",
    });
    document.querySelector(this.CONTAINER_SELECTOR).appendChild(countryPopup);
  },
  onCountryHoverLeave: function (e) {
    var id = e.target.getAttribute("id");
    e.target.setAttribute(
      "stroke-width",
      this.strokeWidth[id] || this.DEFAULT_STROKE_WIDTH
    );
    var el = document.querySelector(this.CONTAINER_SELECTOR);
    var overlayEl = document.getElementById(this.getOverlayHanlderId(id));
    if (el && overlayEl) {
      el.removeChild(overlayEl);
    }
  },
  attachFocusCountriesOnLegendHover: function () {
    // Focusing coutries on legend hover
    var swatchsStrokeWidth = Object.create( null );
    function onSwatchMouseEnter(e) {
      var fill = e.target.getAttribute("fill");
      var elementsSelector = this.CONTAINER_SELECTOR + " " + this.MAP_SELECTOR + " path[fill]:not([fill='" + fill + "'])";
      var elements = document.querySelectorAll(elementsSelector);
      for (var i = 0; i < elements.length; i++) {
      	if (elements[i].parentElement && elements[i].parentElement.id == "swatches") {
      		continue;
      	}
        elements[i].setAttribute("fill-opacity", "0.1");
      }
      var targetElements = document.querySelectorAll(
        this.CONTAINER_SELECTOR + " " + this.MAP_SELECTOR +" path[fill='" + fill + "']"
      );
      for (var i = 0; i < targetElements.length; i++) {
        id = targetElements[i].getAttribute("id");
        strokeWidth = targetElements[i].getAttribute("stroke-width");
        swatchsStrokeWidth[id] = strokeWidth;
        targetElements[i].setAttribute(
          "stroke-width",
          this.HIGHLIGHTED_STROKE_WIDTH
        );
      }
      e.target.setAttribute(
        "stroke-width",
        this.HIGHLIGHTED_STROKE_WIDTH * 1.5
      );
      e.target.style.cursor = "pointer";
    }

    function onSwatchMouseLeave(e) {
      var fill = e.target.getAttribute("fill");
      var elements = document.querySelectorAll(
        this.CONTAINER_SELECTOR + " " + this.MAP_SELECTOR + " path[fill]:not([fill='" + fill + "'])"
      );
      for (var i = 0; i < elements.length; i++) {
        elements[i].setAttribute("fill-opacity", "1");
        id = elements[i].getAttribute("id");
        strokeWidth = swatchsStrokeWidth[id] || this.DEFAULT_STROKE_WIDTH;
        elements[i].setAttribute("stroke-width", strokeWidth);
      }
      var targetElements = document.querySelectorAll(
        this.CONTAINER_SELECTOR + " " + this.MAP_SELECTOR + " path[fill='" + fill + "']"
      );
      for (var i = 0; i < targetElements.length; i++) {
        id = targetElements[i].getAttribute("id");
        strokeWidth = swatchsStrokeWidth[id] || this.DEFAULT_STROKE_WIDTH;
        targetElements[i].setAttribute("stroke-width", strokeWidth);
      }
      e.target.setAttribute("stroke-width", this.DEFAULT_STROKE_WIDTH);
    }

    var swatches = document.querySelectorAll(
      this.CONTAINER_SELECTOR + " #swatches > *"
    );
    for (var i = 0; i < swatches.length; i++) {
      var fill = swatches[i].getAttribute("fill");
      if (!fill || fill.indexOf("#") !== 0) {
        continue;
      }
      swatches[i].onmouseenter = onSwatchMouseEnter.bind(this);
      swatches[i].onmouseleave = onSwatchMouseLeave.bind(this);
    }
  },

  attachClickEventOnCountries: function () {
    document.querySelector(this.CONTAINER_SELECTOR + " " + this.MAP_SELECTOR).onclick =
      function (e) {
        var target = e.target;
        if (target.tagName != "path" || !target.getAttribute("id")) {
          return;
        }
        e.preventDefault();
        e.stopPropagation();
        var clickedId = target.getAttribute("id");
        var clickedCountryCode = OWID_COUNTRY_CODES[clickedId];
        if (clickedCountryCode) {
          this.onCountryHoverLeave(e);
          this.loadCountryChart(clickedCountryCode);
        }
      }.bind(this);
  },

  loadCountryChart: function (countryCode) {
    var url = this.countriesSvgUrls[countryCode];
    if (url) {
      if (this.cachedCountriesSvgs[url]) {
        this.paintCountryChart(this.cachedCountriesSvgs[url]);
        this.$credit[0].href = this.countriesInfoUrls[countryCode];
      } else {
        var that = this;
        this.originalContainerContent = this.$svgContainer.html();
        this.$svgContainer.html(this.$loader.html());
        fetch(url)
          .then(function (data) {
            return data.text();
          })
          .then(function (content) {
            content = OWIDSlider.purify( content );
            that.cachedCountriesSvgs[url] = content;
            that.setSvg(that.originalContainerContent);
            that.paintCountryChart(content);
        	that.$credit[0].href = that.countriesInfoUrls[countryCode];
          })
          .catch(function (err) {
            console.log("Error getting country svg", err);
          });
      }
    } else {
      // Country not imported
      this.paintCountryChart(this.getCountryNotFound());
    }
  },
  getCountryNotFound: function () {
	// Fixme this link doesn't work because the scaledContent event handler blocks it.
    var $viewer = $(
      '<div><p width="850">Country chart not found. You can import it from <a href="https://owidimporter.toolforge.org/" target="_blank" rel="noopener">here</a></p></div>.'
    );
    return $viewer;
  },
populateTranslatedCountriesNames: function() {
    var countryIds = Object.values(OWID_WIKIDATA_COUNTRY_MAP);
    var that = this;
    const chunkSize = 45;
    const chunks = [];
    
    // Split countryIds into chunks
    for (let i = 0; i < countryIds.length; i += chunkSize) {
        chunks.push(countryIds.slice(i, i + chunkSize));
    }
    
    // Process all chunks in parallel with staggered delays
    const chunkPromises = chunks.map((chunk, index) => {
        // Stagger the requests slightly to avoid overwhelming the server
        const delay = index * 50; // 50ms between each chunk start
        
        return new Promise(resolve => setTimeout(resolve, delay)).then(() => {
            const ids = chunk.join('|');
            const url = `https://www.wikidata.org/w/api.php?action=wbgetentities&ids=${ids}&format=json&props=labels&languages=${that.language}`;
            
            var api = new mw.Api({
				userAgent: 'OWIDSlider',
                ajax: {
                    url: 'https://www.wikidata.org/w/api.php'
                }
            });
            
            return api.get({
                action: 'wbgetentities',
                ids: ids,
                format: 'json',
                languages: that.language,
                props: 'labels',
                origin: '*',  // This helps with CORS
				maxage: 24*60*60, // Cache the results. They shouldn't change.
				smaxage: 60*60
            }).then(function(res) {
                const chunkResults = {};
                Object.entries(res.entities).forEach(function([id, entity]) {
                    chunkResults[id] = entity.labels || {};
                });
                return chunkResults;
            }).catch(function(error) {
                console.error('Error fetching labels for chunk:', error);
                return {}; // Return empty object if this chunk fails
            });
        });
    });
    
    // Wait for all chunks to complete and merge results
    return Promise.all(chunkPromises).then(function(allChunkResults) {
        // Merge all chunk results into a single object
        const accumResults = allChunkResults.reduce(function(acc, chunkResult) {
            return Object.assign(acc, chunkResult);
        }, {});
        
        // Process the accumulated results
        Object.entries(accumResults).forEach(function(entry) {
            if (entry[1][that.language]) {
                var name = OWID_WIKIDATA_COUNTRY_MAP_REVERSE[entry[0]];
                name = name.toLowerCase().replace(/[^a-z0-9]/g, '');
                that.translatedCountryNames[name] = entry[1][that.language].value;
            }
        });
        that.toggleImg();
        
        return accumResults;
    }).catch(error => {
        console.error('Error processing translation chunks:', error);
        return null;
    });
},
  getTranslatedCountryName: function(language, name) {
  	var that = this;
  	return new Promise(function(resolve, reject) {
	  	if (!language || !name) {
	  		return reject("Missing language or name");
	  	}
	  	name = name.toLowerCase().replace(/[^a-z0-9]/g, '');
	  	if (!OWID_WIKIDATA_COUNTRY_MAP[name]) {
	  		return reject("Cannot find country name in WIKIDATA ID Map: " + name);
	  	}
	  	if (that.translatedCountryNames[name]) {
	  		return resolve(that.translatedCountryNames[name]);
	  	}
	  	
	  	var countryCode = OWID_WIKIDATA_COUNTRY_MAP[name];
	  	var url = "https://www.wikidata.org/w/rest.php/wikibase/v1/entities/items/" + countryCode + "/labels";
	  	fetch(url, {
	  		headers: {
	    		"accept": "application/json",
			}
	  	})
	      .then(function (response) {
	        return response.json();
	      })
	      .then(function (map) {
	      	if (map && map[language]) {
	      		that.translatedCountryNames[name] = map[language];
	      		return resolve(map[language]);
	      	}
	      	return resolve("");
	      })
	      .catch(function(err) {
	      	console.log("Error getting translated country", err);
	      	reject(err);
	      });
  	});
  },
  paintCountryChart: function (content) {
    this.originalContainerContent = this.$svgContainer.html();
    this.$countrySelect.css("display", "none");
    var scaledContent = this.getScaledSvg(content);

	if (scaledContent.length > 0) {
		var headerSpan = scaledContent.find("#header a text tspan");
		if (headerSpan.length > 0) {
			// Remove the trailing ", $YEAR" from the title if found
    		if (this.worldTitle) {
	    		var titleParts = this.worldTitle.split(",");
	    		if (titleParts.length > 1) {
	    			titleParts.pop();
	    		}
	    		headerSpan.text(titleParts.join(","));			
    		}
		}

		if (this.worldSubtitle) {
    		var subtitleSpan = scaledContent.find("#subtitle text tspan");
    		if (subtitleSpan.length > 0) {
    			subtitleSpan.text(this.worldSubtitle);
    		}
		}
		if (this.worldDetails) {
	    	var currentDetails = scaledContent.find("#details");
	    	if (currentDetails.length > 0) {
	    		currentDetails[0].replaceWith(this.worldDetails);
	    	}
	    }
	    
	    // Handle translation
	    if (this.language && this.language != "en") {
	    	var countryLabel = scaledContent.find("#text-labels text tspan");
	    	var that = this;
	    	if (countryLabel.length > 0) {
	    		var countryName = countryLabel[0].textContent;
	    		this.getTranslatedCountryName(this.language, countryName)
	    		.then(function(translatedName) {
	    			if (translatedName) {
	    				countryLabel[0].textContent = translatedName;
	    			}

	    			that.applyCountryChartPaint(content, scaledContent);
	    		})
	    		.catch(function(err) {
	    			console.log("Error getting translated country name: ", err);
	    			that.applyCountryChartPaint(content, scaledContent);
	    		});
	    		return;
	    	}
	    }
	}
   this.applyCountryChartPaint(content, scaledContent);
  },
  applyCountryChartPaint: function(content, scaledContent) {
    scaledContent.on("click", function (e) {
      e.preventDefault();
      e.stopPropagation();
    });
    this.$svgContainer.html("").append(scaledContent);
    
    // Back content
    var $back = $("<button></button>")
      .attr({
        type: "button",
        class: "OWIDSlider-country-back",
        title: mw.msg("OWIDSliderFrameBack"),
        "aria-label": mw.msg("OWIDSliderFrameBack"),
      })
      .text(mw.msg("OWIDSliderFrameBack"));
    var $backContainer = $("<div></div>")
      .attr({ class: "OWIDSlider-country-back-container" })
      .append($back);
    $backContainer.css("margin-top", "10px");
    $back.on(
      "click",
      function () {
    	this.$svgContainer.html("").append(this.originalContainerContent);
		this.$credit[0].href = this.infoUrls[this.currentView][this.currentImage];
		$backContainer.remove();
        setTimeout(
          function () {
			this.$countrySelect.css("display", "inline-block");
            this.initSVGControls();
          }.bind(this),
          100
        );
      }.bind(this)
    );
    $(".OWIDSliderSVGContainer").before($backContainer);
  },
};

// Include jquery.mousewheel dependency.
// --------
/*! Copyright (c) 2013 Brandon Aaron (http://brandon.aaron.sh)
 * Licensed under the MIT License (LICENSE.txt).
 *
 * Version: 3.1.11
 *
 * Requires: jQuery 1.2.2+
 */

(function (factory) {
  if (typeof define === "function" && define.amd) {
    // AMD. Register as an anonymous module.
    define(["jquery"], factory);
  } else if (typeof exports === "object") {
    // Node/CommonJS style for Browserify
    module.exports = factory;
  } else {
    // Browser globals
    factory(jQuery);
  }
})(function ($) {
  var toFix = ["wheel", "mousewheel", "DOMMouseScroll", "MozMousePixelScroll"],
    toBind =
      "onwheel" in document || document.documentMode >= 9
        ? ["wheel"]
        : ["mousewheel", "DomMouseScroll", "MozMousePixelScroll"],
    slice = Array.prototype.slice,
    nullLowestDeltaTimeout,
    lowestDelta;

  if ($.event.fixHooks) {
    for (var i = toFix.length; i; ) {
      $.event.fixHooks[toFix[--i]] = $.event.mouseHooks;
    }
  }

  var special = ($.event.special.mousewheel = {
    version: "3.1.11",

    setup: function () {
      if (this.addEventListener) {
        for (var i = toBind.length; i; ) {
          this.addEventListener(toBind[--i], handler, false);
        }
      } else {
        this.onmousewheel = handler;
      }
      // Store the line height and page height for this particular element
      $.data(this, "mousewheel-line-height", special.getLineHeight(this));
      $.data(this, "mousewheel-page-height", special.getPageHeight(this));
    },

    teardown: function () {
      if (this.removeEventListener) {
        for (var i = toBind.length; i; ) {
          this.removeEventListener(toBind[--i], handler, false);
        }
      } else {
        this.onmousewheel = null;
      }
      // Clean up the data we added to the element
      $.removeData(this, "mousewheel-line-height");
      $.removeData(this, "mousewheel-page-height");
    },

    getLineHeight: function (elem) {
      var $parent =
        $(elem)["offsetParent" in $.fn ? "offsetParent" : "parent"]();
      if (!$parent.length) {
        $parent = $("body");
      }
      return parseInt($parent.css("fontSize"), 10);
    },

    getPageHeight: function (elem) {
      return $(elem).height();
    },

    settings: {
      adjustOldDeltas: true, // see shouldAdjustOldDeltas() below
      normalizeOffset: true, // calls getBoundingClientRect for each event
    },
  });

  $.fn.extend({
    mousewheel: function (fn) {
      return fn ? this.on("mousewheel", fn) : this.trigger("mousewheel");
    },

    unmousewheel: function (fn) {
      return this.off("mousewheel", fn);
    },
  });

  function handler(event) {
    var orgEvent = event || window.event,
      args = slice.call(arguments, 1),
      delta = 0,
      deltaX = 0,
      deltaY = 0,
      absDelta = 0,
      offsetX = 0,
      offsetY = 0;
    event = $.event.fix(orgEvent);
    event.type = "mousewheel";

    // Old school scrollwheel delta
    if ("detail" in orgEvent) {
      deltaY = orgEvent.detail * -1;
    }
    if ("wheelDelta" in orgEvent) {
      deltaY = orgEvent.wheelDelta;
    }
    if ("wheelDeltaY" in orgEvent) {
      deltaY = orgEvent.wheelDeltaY;
    }
    if ("wheelDeltaX" in orgEvent) {
      deltaX = orgEvent.wheelDeltaX * -1;
    }

    // Firefox < 17 horizontal scrolling related to DOMMouseScroll event
    if ("axis" in orgEvent && orgEvent.axis === orgEvent.HORIZONTAL_AXIS) {
      deltaX = deltaY * -1;
      deltaY = 0;
    }

    // Set delta to be deltaY or deltaX if deltaY is 0 for backwards compatabilitiy
    delta = deltaY === 0 ? deltaX : deltaY;

    // New school wheel delta (wheel event)
    if ("deltaY" in orgEvent) {
      deltaY = orgEvent.deltaY * -1;
      delta = deltaY;
    }
    if ("deltaX" in orgEvent) {
      deltaX = orgEvent.deltaX;
      if (deltaY === 0) {
        delta = deltaX * -1;
      }
    }

    // No change actually happened, no reason to go any further
    if (deltaY === 0 && deltaX === 0) {
      return;
    }

    // Need to convert lines and pages to pixels if we aren't already in pixels
    // There are three delta modes:
    //   * deltaMode 0 is by pixels, nothing to do
    //   * deltaMode 1 is by lines
    //   * deltaMode 2 is by pages
    if (orgEvent.deltaMode === 1) {
      var lineHeight = $.data(this, "mousewheel-line-height");
      delta *= lineHeight;
      deltaY *= lineHeight;
      deltaX *= lineHeight;
    } else if (orgEvent.deltaMode === 2) {
      var pageHeight = $.data(this, "mousewheel-page-height");
      delta *= pageHeight;
      deltaY *= pageHeight;
      deltaX *= pageHeight;
    }

    // Store lowest absolute delta to normalize the delta values
    absDelta = Math.max(Math.abs(deltaY), Math.abs(deltaX));

    if (!lowestDelta || absDelta < lowestDelta) {
      lowestDelta = absDelta;

      // Adjust older deltas if necessary
      if (shouldAdjustOldDeltas(orgEvent, absDelta)) {
        lowestDelta /= 40;
      }
    }

    // Adjust older deltas if necessary
    if (shouldAdjustOldDeltas(orgEvent, absDelta)) {
      // Divide all the things by 40!
      delta /= 40;
      deltaX /= 40;
      deltaY /= 40;
    }

    // Get a whole, normalized value for the deltas
    delta = Math[delta >= 1 ? "floor" : "ceil"](delta / lowestDelta);
    deltaX = Math[deltaX >= 1 ? "floor" : "ceil"](deltaX / lowestDelta);
    deltaY = Math[deltaY >= 1 ? "floor" : "ceil"](deltaY / lowestDelta);

    // Normalise offsetX and offsetY properties
    if (special.settings.normalizeOffset && this.getBoundingClientRect) {
      var boundingRect = this.getBoundingClientRect();
      offsetX = event.clientX - boundingRect.left;
      offsetY = event.clientY - boundingRect.top;
    }

    // Add information to the event object
    event.deltaX = deltaX;
    event.deltaY = deltaY;
    event.deltaFactor = lowestDelta;
    event.offsetX = offsetX;
    event.offsetY = offsetY;
    // Go ahead and set deltaMode to 0 since we converted to pixels
    // Although this is a little odd since we overwrite the deltaX/Y
    // properties with normalized deltas.
    event.deltaMode = 0;

    // Add event and delta to the front of the arguments
    args.unshift(event, delta, deltaX, deltaY);

    // Clearout lowestDelta after sometime to better
    // handle multiple device types that give different
    // a different lowestDelta
    // Ex: trackpad = 3 and mouse wheel = 120
    if (nullLowestDeltaTimeout) {
      clearTimeout(nullLowestDeltaTimeout);
    }
    nullLowestDeltaTimeout = setTimeout(nullLowestDelta, 200);

    return ($.event.dispatch || $.event.handle).apply(this, args);
  }

  function nullLowestDelta() {
    lowestDelta = null;
  }

  function shouldAdjustOldDeltas(orgEvent, absDelta) {
    // If this is an older event and the delta is divisable by 120,
    // then we are assuming that the browser is treating this as an
    // older mouse wheel event and that we should divide the deltas
    // by 40 to try and get a more usable deltaFactor.
    // Side note, this actually impacts the reported scroll distance
    // in older browsers and can cause scrolling to be slower than native.
    // Turn this off by setting $.event.special.mousewheel.settings.adjustOldDeltas to false.
    return (
      special.settings.adjustOldDeltas &&
      orgEvent.type === "mousewheel" &&
      absDelta % 120 === 0
    );
  }
});

// --- Start image stack popup
$(OWIDSlider.init); // Script written by Bawolff for WikiProject Med Foundation based on earlier OWIDSlider script by Hellerhoff.