Jump to content

MediaWiki:Gadget-owidslider.js

From Wikipedia, the free encyclopedia
The printable version is no longer supported and may have rendering errors. Please update your browser bookmarks and please use the default browser print function instead.
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.