/**
 * MapSVG 6.3.1 - Interactive Map Plugin
 *
 * Author: Roman S. Stepanov
 * http://codecanyon.net/user/RomanCode/portfolio?ref=RomanCode
 *
 * MapSVG @CodeCanyon: http://codecanyon.net/item/jquery-interactive-svg-map-plugin/1694201?ref=RomanCode
 * Licenses: http://codecanyon.net/licenses/regular_extended?ref=RomanCode
 */

;(function ($) {
  var mapSVG = {}

  function isNumber(n) {
    return !isNaN(parseFloat(n)) && isFinite(n)
  }

  function ucfirst(string) {
    return string.charAt(0).toUpperCase() + string.slice(1)
  }

  function parseBoolean(string) {
    switch (String(string).toLowerCase()) {
      case "on":
      case "true":
      case "1":
      case "yes":
      case "y":
        return true
      case "off":
      case "false":
      case "0":
      case "no":
      case "n":
        return false
      default:
        return undefined
    }
  }

  function safeURL(url) {
    if (url.indexOf("http://") == 0 || url.indexOf("https://") == 0)
      url = "//" + url.split("://").pop()
    return url
  }

  function extend(sub, base) {
    sub.prototype = Object.create(base.prototype)
    sub.prototype.constructor = sub
  }

  function MapObject() {
    this.id = ""
  }

  MapObject.prototype.getComputedStyle = function (prop, node) {
    node = node || this.node[0]

    if ((_p1 = node.getAttribute(prop))) {
      return _p1
    } else if ((_p2 = node.getAttribute("style"))) {
      var s = _p2.split(";")
      var z = s.filter(function (e) {
        var e = e.trim()
        var attr = e.split(":")
        if (attr[0] == prop) return true
      })
      if (z.length) {
        return z[0].split(":").pop().trim()
      }
    }

    var parent = $(node).parent()
    var nodeType = parent.length ? parent[0].tagName : null

    if (nodeType && nodeType != "svg") return this.getComputedStyle(prop, parent[0])
    else return undefined
  }

  MapObject.prototype.getStyle = function (prop) {
    if ((_p1 = this.attr(prop))) {
      return _p1
    } else if ((_p2 = this.attr("style"))) {
      var s = _p2.split(";")
      var z = s.filter(function (e) {
        var e = e.trim()
        if (e.indexOf(prop) === 0) return e
      })

      return z.length ? z[0].split(":").pop().trim() : undefined
    }
    return ""
  }
  MapObject.prototype.getCenter = function () {
    var x = this.node[0].getBoundingClientRect().left
    var y = this.node[0].getBoundingClientRect().top
    var w = this.node[0].getBoundingClientRect().width
    var h = this.node[0].getBoundingClientRect().height
    return [x + w / 2, y + h / 2]
  }
  MapObject.prototype.setTooltip = function (text) {
    this.tooltip = text ? text : undefined
  }
  MapObject.prototype.setPopover = function (text) {
    this.popover = text ? text : undefined
  }
  MapObject.prototype.setHref = function (url) {
    if (this.href && this.node.parent("a").length) this.node.unwrap()
    this.href = url ? url : undefined
    if (this.href) {
      var xlinkNS = "http://www.w3.org/1999/xlink",
        svgNS = "http://www.w3.org/2000/svg"
      var a = document.createElementNS(svgNS, "a")
      a.setAttributeNS(xlinkNS, "href", this.href)
      if (this.target) a.setAttribute("target", this.target)
      this.node.wrap($(a))
    }
  }
  MapObject.prototype.setTarget = function (target) {
    this.target = target ? target : undefined
    if (this.href) var a = this.node.parent("a")[0]
    else return
    this.target ? a.setAttribute("target", target) : a.removeAttribute("target")
  }
  MapObject.prototype.setData = function (data) {
    if (data) {
      if (typeof data == "string") {
        if (data.substr(0, 1) == "[" || data.substr(0, 1) == "{") {
          try {
            var tmp
            eval("tmp = " + data)
            this.data = tmp
          } catch (err) {
            var error =
              "MapSVG: Error in Data object for " +
              this.mapsvg_type +
              " '" +
              this.id +
              "'. Data object was set to empty object {}."
            this.data = {}
            return { _error: error }
          }
        } else {
          this.data = data
        }
      } else {
        this.data = data
      }
      return this.data
    } else {
      this.data = undefined
    }
  }
  MapObject.prototype.attr = function (v1, v2) {
    var svgDom = this.node[0]

    if (typeof v1 == "object") {
      $.each(v1, function (key, item) {
        if (typeof item == "string" || typeof item == "number") {
          svgDom.setAttribute(key, item)
        }
      })
    } else if (typeof v1 == "string" && (typeof v2 == "string" || typeof v2 == "number")) {
      svgDom.setAttribute(v1, v2)
    } else if (v2 == undefined) {
      return svgDom.getAttribute(v1)
    }
  }
  MapObject.prototype.setId = function (id) {
    if (!id) return false
    this.id = id
    this.node[0].setAttribute("id", id)
  }

  function Region(jQueryObject, globalOptions, regionID, mapsvg) {
    MapObject.call(this)

    this.node = jQueryObject
    this.nodeType = jQueryObject[0].tagName
    this.globalOptions = globalOptions
    this.mapsvg = mapsvg

    this.id = this.node.attr("id")

    if (!this.id) {
      this.setId(this.nodeType + "_" + regionID.id++)
      this.autoID = true
    }

    this.title = this.node.attr("title")

    this.node[0].setAttribute("class", "mapsvg-region")
    this.svg_style = { fill: this.getComputedStyle("fill") }

    this.svg_style.stroke = this.getComputedStyle("stroke")
    // Make stroke-width always the same:
    if (!_browser.ie)
      // && !_browser.firefox)
      this.node.css({ "vector-effect": "non-scaling-stroke" })
    else {
      var w = this.getComputedStyle("stroke-width")
      w = w ? w.replace("px", "") : "1"
      w = w == "1" ? 1.2 : parseFloat(w)
      this.svg_style["stroke-width"] = w
    }

    var regionOptions =
      globalOptions.regions && globalOptions.regions[this.id]
        ? globalOptions.regions[this.id]
        : null

    this.disabled = this.getDisabledState()
    this.disabled && this.attr("class", this.attr("class") + " mapsvg-disabled")

    this.default_attr = {}
    this.selected_attr = {}
    this.hover_attr = {}
    this.mapsvg_type = "region"
    var selected = false
    if (regionOptions && regionOptions.selected) {
      selected = true
      delete regionOptions.selected
    }
    regionOptions && this.update(regionOptions)
    this.setFill()
    if (selected) this.setSelected(true)
    this.saveState()
  }
  extend(Region, MapObject)

  Region.prototype.saveState = function () {
    this.initialState = JSON.stringify(this.getOptions())
  }
  Region.prototype.changed = function () {
    return JSON.stringify(this.getOptions()) != this.initialState
  }
  Region.prototype.getOptions = function (forTemplate) {
    var globals = this.globalOptions.regions[this.id]
    var o = {
      id: this.id,
      id_no_spaces: this.id_no_spaces,
      title: this.title,
      disabled: this.disabled === this.getDisabledState(true) ? undefined : this.disabled,
      fill: this.globalOptions.regions[this.id] && this.globalOptions.regions[this.id].fill,
      tooltip: this.tooltip,
      popover: this.popover,
      href: this.href,
      target: this.target,
      data: this.data,
      gaugeValue: this.gaugeValue,
    }
    if (forTemplate) o.disabled = this.disabled
    $.each(o, function (key, val) {
      if (val == undefined) {
        delete o[key]
      }
    })
    return o
  }

  Region.prototype.update = function (options) {
    for (var key in options) {
      // check if there's a setter for a property
      var setter = "set" + ucfirst(key)
      if (setter in this) this[setter](options[key])
      //else
      //    console.log('MapSVG error: - no setter Region.'+setter);
    }
  }

  Region.prototype.setFill = function (fill) {
    if (fill) {
      regions = {}
      regions[this.id] = { fill: fill }
      $.extend(true, this.globalOptions, { regions: regions })
    } else if (
      fill == "" &&
      this.globalOptions.regions &&
      this.globalOptions.regions[this.id] &&
      this.globalOptions.regions[this.id].fill
    ) {
      //regions = {};
      //regions[this.id] = {fill: undefined};
      //$.extend(true, this.globalOptions, {regions: regions});
      delete this.globalOptions.regions[this.id].fill
    }

    // Priority: gauge > options.fill > disabled > base > svg
    if (this.globalOptions.gauge.on && this.gaugeValue) {
      var o = this.globalOptions.gauge
      var w = (this.gaugeValue - o.min) / o.maxAdjusted

      var rgb = {
        r: Math.round(o.colors.diffRGB.r * w + o.colors.lowRGB.r),
        g: Math.round(o.colors.diffRGB.g * w + o.colors.lowRGB.g),
        b: Math.round(o.colors.diffRGB.b * w + o.colors.lowRGB.b),
        a: Math.round(o.colors.diffRGB.a * w + o.colors.lowRGB.a),
      }
      this.default_attr["fill"] = "rgba(" + rgb.r + "," + rgb.g + "," + rgb.b + "," + rgb.a + ")"
    } else if (this.globalOptions.regions[this.id] && this.globalOptions.regions[this.id].fill) {
      this.default_attr["fill"] = this.globalOptions.regions[this.id].fill
    } else if (this.disabled && this.globalOptions.colors.disabled) {
      this.default_attr["fill"] = this.globalOptions.colors.disabled
    } else if (this.globalOptions.colors.base) {
      this.default_attr["fill"] = this.globalOptions.colors.base
    } else if (this.svg_style.fill != "none") {
      this.default_attr["fill"] = this.svg_style.fill
        ? this.svg_style.fill
        : this.globalOptions.colors.baseDefault
    } else {
      this.default_attr["fill"] = "none"
    }

    if (isNumber(this.globalOptions.colors.selected))
      this.selected_attr["fill"] = tinycolor(this.default_attr.fill)
        .lighten(parseFloat(this.globalOptions.colors.selected))
        .toRgbString()
    else this.selected_attr["fill"] = this.globalOptions.colors.selected

    if (isNumber(this.globalOptions.colors.hover))
      this.hover_attr["fill"] = tinycolor(this.default_attr.fill)
        .lighten(parseFloat(this.globalOptions.colors.hover))
        .toRgbString()
    else this.hover_attr["fill"] = this.globalOptions.colors.hover

    this.node.css("fill", this.default_attr["fill"])
    this.fill = this.default_attr["fill"]

    if (this.svg_style.stroke != "none" && this.globalOptions.colors.stroke != undefined)
      this.node.css("stroke", this.globalOptions.colors.stroke)
    else this.node.css("stroke", this.svg_style.stroke)

    if (this.selected) this.select()
  }
  Region.prototype.setDisabled = function (on) {
    on = on !== undefined ? parseBoolean(on) : this.getDisabledState() // get default disabled state if undefined
    this.disabled = on
    on
      ? this.attr("class", this.attr("class") + " mapsvg-disabled")
      : this.attr("class", this.attr("class").replace(" mapsvg-disabled", ""))
    this.selected = false
    this.setFill()
  }
  Region.prototype.setSelected = function (on) {
    //this.selected = parseBoolean(on);
    this.mapsvg.selectRegion(this)
  }
  Region.prototype.setGaugeValue = function (val) {
    this.gaugeValue = $.isNumeric(val) ? parseFloat(val) : undefined
  }
  Region.prototype.getDisabledState = function (asDefault) {
    var opts = this.globalOptions.regions[this.id]
    if (!asDefault && opts && opts.disabled !== undefined) {
      return opts.disabled
    } else if (
      this.globalOptions.disableAll ||
      this.svg_style.fill == "none" ||
      this.id == "labels" ||
      this.id == "Labels"
    ) {
      return true
    } else {
      return false
    }
  }
  Region.prototype.highlight = function () {
    this.node.css({ fill: this.hover_attr.fill })
  }
  Region.prototype.unhighlight = function () {
    this.node.css({ fill: this.default_attr.fill })
  }
  Region.prototype.select = function () {
    this.node.css({ fill: this.selected_attr.fill })
    this.selected = true
  }
  Region.prototype.deselect = function () {
    this.node.css({ fill: this.default_attr.fill })
    this.selected = false
  }

  // MARKER
  function Marker(options, scale) {
    if (!options.id || !options.src) return false

    MapObject.call(this)

    var img = document.createElementNS("http://www.w3.org/2000/svg", "image")
    img.setAttributeNS(null, "height", options.height)
    img.setAttributeNS(null, "width", options.width)
    img.setAttributeNS("http://www.w3.org/1999/xlink", "href", options.src)
    img.setAttributeNS(null, "x", options.x)
    img.setAttributeNS(null, "y", options.y)
    img.setAttributeNS(null, "visibility", "visible")
    img.setAttribute("class", "mapsvg-marker")
    this.node = $(img)

    this.src = options.src
    this.setId(options.id)
    this.mapsvg_type = "marker"
    this.x = parseFloat(options.x)
    this.y = parseFloat(options.y)
    this.width = parseFloat(options.width)
    this.height = parseFloat(options.height)
    this.tooltip = options.tooltip
    this.popover = options.popover
    this.href = options.href
    this.target = options.target
    this.default = { x: this.x, y: this.y, width: this.width, height: this.height }
    this.geoCoords = options.geoCoords
    this.data = options.data
  }
  extend(Marker, MapObject)

  Marker.prototype.getOptions = function () {
    var o = {
      id: this.id,
      tooltip: this.tooltip,
      popover: this.popover,
      href: this.href,
      target: this.target,
      data: this.data,
      src: this.src,
      width: this.default.width,
      height: this.default.height,
      x: this.x,
      y: this.y,
      geoCoords: this.geoCoords,
    }
    $.each(o, function (key, val) {
      if (val == undefined) {
        delete o[key]
      }
    })
    return o
  }

  Marker.prototype.setXY = function (x, y) {
    this.x = x
    this.y = y
    this.node[0].setAttribute("x", x)
    this.node[0].setAttribute("y", y)
    this.adjustPosition(this.mapScale)
  }

  Marker.prototype.update = function (data, mapScale) {
    for (var key in data) {
      // check if there's a setter for a property
      var setter = "set" + ucfirst(key)
      if (setter in this) this[setter](data[key], mapScale)
    }
  }
  Marker.prototype.setSrc = function (src, mapScale) {
    if (!src) return false
    src = safeURL(src)
    mapScale = mapScale || this.mapScale
    var img = new Image()
    var marker = this
    img.onload = function () {
      marker.default.width = this.width
      marker.default.height = this.height

      marker.x = marker.x - (this.width - marker.width) / 2
      marker.y = marker.y - (this.height - marker.height)

      marker.attr({ x: marker.x, y: marker.y, width: this.width, height: this.height })
      marker.width = this.width
      marker.height = this.height

      marker.node[0].setAttributeNS("http://www.w3.org/1999/xlink", "href", src)
      marker.adjustPosition(mapScale)

      marker.src = src
    }
    img.src = src
  }
  Marker.prototype.adjustPosition = function (mapScale) {
    var w = this.default.width
    var h = this.default.height
    var dx = w / 2 - w / (2 * mapScale)
    var dy = h - h / mapScale
    //this.x += w/2 - w/(2*mapScale);
    //this.y += h - h/mapScale;
    this.attr("width", w / mapScale)
    this.attr("height", h / mapScale)
    //this.attr('x',this.x);
    //this.attr('y',this.y + dx);
    this.attr("transform", "translate(" + dx + "," + dy + ")")
    this.mapScale = mapScale
  }
  // GET MARK COORDINATES TRANSLATED TO 1:1 SCALE (used when saving new added markers)
  Marker.getDefaultCoords = function (markerX, markerY, markerWidth, markerHeight, mapScale) {
    markerX = parseFloat(markerX)
    markerY = parseFloat(markerY)
    markerWidth = parseFloat(markerWidth)
    markerHeight = parseFloat(markerHeight)
    markerX = markerX + markerWidth / (2 * mapScale) - markerWidth / 2
    markerY = markerY + markerHeight / mapScale - markerHeight
    return [markerX, markerY]
  }
  Marker.prototype.drag = function (startCoords, scale, endCallback, clickCallback) {
    _this = this
    this.ox = this.x
    this.oy = this.y

    // TODO cursor
    //$('body').css('cursor','move');
    //this.node.closest('svg').css('pointer-events','none');

    $("body").on("mousemove.drag.mapsvg", function (e) {
      e.preventDefault()
      //$('body').css('cursor','move');
      var mouseNew = mouseCoords(e)
      var dx = mouseNew.x - startCoords.x
      var dy = mouseNew.y - startCoords.y
      _this.x = _this.ox + dx / scale
      _this.y = _this.oy + dy / scale
      _this.attr({ x: _this.x, y: _this.y })
      //_this.attr('transform','translate('+dx/scale+','+dy/scale+')');
    })
    $("body").on("mouseup.drag.mapsvg", function (e) {
      e.preventDefault()
      _this.undrag()
      var mouseNew = mouseCoords(e)
      var dx = mouseNew.x - startCoords.x
      var dy = mouseNew.y - startCoords.y
      _this.x = _this.ox + dx / scale
      _this.y = _this.oy + dy / scale
      _this.attr({ x: _this.x, y: _this.y })
      endCallback.call(_this)
      if (_this.ox == _this.x && _this.oy == _this.y) clickCallback.call(_this)
    })
  }
  Marker.prototype.undrag = function () {
    //this.node.closest('svg').css('pointer-events','auto');
    //$('body').css('cursor','default');
    $("body").off(".drag.mapsvg")
  }
  Marker.prototype.delete = function () {
    if (this.href) this.node.parent("a").empty().remove()
    this.node.empty().remove()
  }

  var instances = {}
  var globalID = 0
  var userAgent = navigator.userAgent.toLowerCase()

  // Get plugin's path
  var scripts = document.getElementsByTagName("script")
  var myScript = scripts[scripts.length - 1].src.split("/")
  myScript.pop()
  var pluginJSURL = myScript.join("/") + "/"
  myScript.pop()
  var pluginRootURL = myScript.join("/") + "/"

  // Check for iPad/Iphone/Andriod
  var touchDevice =
    userAgent.indexOf("ipad") > -1 ||
    userAgent.indexOf("iphone") > -1 ||
    userAgent.indexOf("ipod") > -1 ||
    userAgent.indexOf("android") > -1

  var _browser = {}

  _browser.ie =
    userAgent.indexOf("msie") > -1 ||
    userAgent.indexOf("trident") > -1 ||
    userAgent.indexOf("edge") > -1
      ? {}
      : false
  _browser.firefox = userAgent.indexOf("firefox") > -1

  if (!String.prototype.trim) {
    String.prototype.trim = function () {
      return this.replace(/^\s+|\s+$/g, "")
    }
  }

  // Create function for retrieving mouse coordinates
  var mouseCoords = function (e) {
    if (e.clientX) {
      return {
        x: e.clientX + $(document).scrollLeft(),
        y: e.clientY + $(document).scrollTop(),
      }
    }
    if (e.pageX) {
      return { x: e.pageX, y: e.pageY }
    } else if (touchDevice) {
      e = e.originalEvent
      return e.touches && e.touches[0]
        ? { x: e.touches[0].pageX, y: e.touches[0].pageY }
        : { x: e.changedTouches[0].pageX, y: e.changedTouches[0].pageY }
    }
  }

  // Default options
  var defaults = {
    markerLastID: 0,
    regionLastID: 0,
    disableAll: false,
    width: null,
    height: null,
    lockAspectRatio: true,
    loadingText: "Loading map...",
    //colors              : {base: "#E1F1F1", background: "#eeeeee", hover: "#548eac", selected: "#065A85", stroke: "#7eadc0"},
    colors: { baseDefault: "#000000", background: "#eeeeee", selected: 40, hover: 20 },
    regions: {},
    markers: [],
    //markerGroups        : {}, // {group_id: [marker_id, marker_id2], group_id2: [...]}
    //regionGroups        : {},
    viewBox: [],
    cursor: "default",
    onClick: null,
    mouseOver: null,
    mouseOut: null,
    beforeLoad: null,
    afterLoad: null,
    zoom: {
      on: false,
      limit: [0, 10],
      delta: 1.2,
      buttons: { on: true, location: "right" },
      mousewheel: true,
    },
    scroll: { on: false, limit: false, background: false },
    responsive: true,
    tooltips: { mode: "off", on: false, priority: "local", position: "bottom-right" },
    popovers: { mode: "off", on: false, priority: "local", position: "top" },
    multiSelect: false,
    gauge: {
      on: false,
      labels: { low: "low", high: "high" },
      colors: { lowRGB: null, highRGB: null, low: "#550000", high: "#ee0000" },
      min: 0,
      max: 0,
    },
    menu: {
      on: false,
      containerId: "mapsvg-menu-regions",
      template: function (region) {
        return '<li><a href="#' + region.id + '">' + (region.title || region.id) + "</a></li>"
      },
    },
    menuMarkers: {
      on: false,
      containerId: "mapsvg-menu-markers",
      template: function (marker) {
        return '<li><a href="#' + marker.id + '">' + marker.id + "</a></li>"
      },
    },
  }

  // Default marker style
  var markerOptions = { src: pluginRootURL + "markers/pin1_red.png" }

  /** Main Class **/
  mapSVG = function (elem, options) {
    var _data

    this.methods = {
      mouseCoords: mouseCoords,
      functionFromString: function (string) {
        var func
        var error = false
        var fn = string
        if (fn.indexOf("{") == -1 || fn.indexOf("function") == -1 || fn.indexOf("(") == -1) {
          return { error: "MapSVG user function error: no function body." }
        }
        var fnBody = fn.substring(fn.indexOf("{") + 1, fn.lastIndexOf("}"))
        var params = fn.substring(fn.indexOf("(") + 1, fn.indexOf(")"))
        try {
          func = new Function(params, fnBody)
          success = true
        } catch (err) {
          error = err
        }
        if (!error) return func
        else
          return {
            error: {
              line: error.line,
              text: "MapSVG user function error: (line " + error.line + "): " + error.message,
            },
          }
      },
      getOptions: function (forTemplate, forWeb) {
        var options = $.extend(true, {}, _data.options)
        options.viewBox = _data._viewBox

        delete options.markers
        //var region = {id: "", title: "", disabled: false, selected: false,
        if (forTemplate) {
          options.regions = []
          _data.regions.forEach(function (r) {
            options.regions.push(r.getOptions())
          })
          options.markers = _data.options.markers
        } else {
          _data.regions.forEach(function (r) {
            r.changed() && (options.regions[r.id] = r.getOptions())
          })
          if (_data.markers.length > 0) options.markers = []
          _data.markers.forEach(function (m) {
            options.markers.push(m.getOptions())
          })
        }

        if (forWeb)
          $.each(options, function (key, val) {
            if (JSON.stringify(val) == JSON.stringify(defaults[key])) delete options[key]
          })
        return options
      },
      // SETTERS
      update: function (options) {
        for (var key in options) {
          if (key == "regions") {
            $.each(options.regions, function (id, regionOptions) {
              var region = _this.getRegion(id)
              region && region.update(regionOptions)
              if (regionOptions.gaugeValue != undefined) {
                _this.updateGaugeMinMax()
                _this.regionsRedrawColors()
              }
              if (regionOptions.disabled != undefined) {
                _this.deselectRegion(region)
                _data.options.regions[id] = _data.options.regions[id] || {}
                _data.options.regions[id].disabled = region.disabled
              }
            })
          } else if (key == "markers") {
            $.each(options.markers, function (id, markerOptions) {
              var marker = _this.getMarker(id)
              if (markerOptions.geoCoords) {
                if (typeof markerOptions.geoCoords == "string") {
                  markerOptions.geoCoords = markerOptions.geoCoords.trim().split(",")
                  markerOptions.geoCoords = [
                    parseFloat(markerOptions.geoCoords[0]),
                    parseFloat(markerOptions.geoCoords[1]),
                  ]
                }
                if (
                  typeof markerOptions.geoCoords == "object" &&
                  markerOptions.geoCoords.length == 2
                ) {
                  if (
                    $.isNumeric(markerOptions.geoCoords[0]) &&
                    $.isNumeric(markerOptions.geoCoords[1])
                  ) {
                    var xy = _this.convertGeoToPixel(markerOptions.geoCoords)
                    xy[0] = xy[0] - marker.width / 2 + 0.3
                    xy[1] = xy[1] - marker.height + 1.5
                    marker.setXY(xy[0], xy[1])
                  }
                  //delete markerOptions.geoCoords;
                }
              } else {
                marker && marker.update(markerOptions)
              }
            })
          } else {
            var setter = "set" + ucfirst(key)
            if (_this.hasOwnProperty(setter)) this[setter](options[key])
            else console.log("MapSVG Error: no setter " + setter + "()")
          }
        }
      },
      setTitle: function (title) {
        title && (_data.options.title = title)
      },
      setDisableLinks: function (on) {
        on = parseBoolean(on)
        if (on) {
          _data.$map.on("click.a.mapsvg", "a", function (e) {
            e.preventDefault()
          })
        } else {
          _data.$map.off("click.a.mapsvg")
        }
        _data.disableLinks = on
      },
      setLoadingText: function (val) {
        _data.options.loadingText = val
      },
      setLockAspectRatio: function (val) {
        _data.options.lockAspectRatio = parseBoolean(val)
      },
      setOnClick: function (h) {
        _data.options.onClick = h || undefined
      },
      setMouseOver: function (h) {
        _data.options.mouseOver = h || undefined
      },
      setMouseOut: function (h) {
        _data.options.mouseOut = h || undefined
      },
      setBeforeLoad: function (h) {
        _data.options.beforeLoad = h || undefined
      },
      setAfterLoad: function (h) {
        _data.options.afterLoad = h || undefined
      },
      setMarkerEditHandler: function (handler) {
        _data.markerEditHandler = handler
      },
      setRegionEditHandler: function (handler) {
        _data.regionEditHandler = handler
      },
      setDisableAll: function (on) {
        on = parseBoolean(on)
        $.extend(true, _data.options, { disableAll: on })
        _data.regions.forEach(function (r) {
          r.setDisabled()
        })
      },
      setColors: function (colors) {
        $.extend(true, _data.options, { colors: colors })
        //_data.$map.css({'background': _data.options.colors.background});
        //if(colors.stroke)
        //    _data.regions.forEach(function(r){
        //        //if (r.default_attr['stroke'] == _data.options.colors.stroke)
        //        //    r.default_attr['stroke'] = color;
        //        r.node.css('stroke',colors.stroke);
        //    });
        $.each(_data.options.colors, function (key, color) {
          if (color === null || color == "") delete color[key]
        })
        if (colors.background) _data.$map.css({ background: _data.options.colors.background })
        if (colors.hover)
          _data.options.colors.hover =
            colors.hover == "" + parseInt(colors.hover) ? parseInt(colors.hover) : colors.hover
        if (colors.selected)
          _data.options.colors.selected =
            colors.selected == "" + parseInt(colors.selected)
              ? parseInt(colors.selected)
              : colors.selected
        _this.regionsRedrawColors()
      },
      setTooltips: function (options) {
        if (typeof options.mode == "string" && options.mode.indexOf("function") == 0)
          options.mode = _this.functionFromString(options.mode)

        _data.options.tooltips.on = _data.options.tooltips.mode != "off"

        $.extend(true, _data.options, { tooltips: options })

        if (!_data.mapTip) {
          _data.mapTip = $("<div />").addClass("mapsvg-tooltip")
          $("body").append(_data.mapTip)
        }
        var event = "mousemove.tooltip.mapsvg-" + _data.$map.attr("id")
        $("body").off(event)
        var tip = _data.mapTip[0]
        switch (_data.options.tooltips.position) {
          case "bottom-right":
            $("body").on(event, function (e) {
              window.m = mouseCoords(e)
              tip.style.left = e.clientX + $(window).scrollLeft() + "px"
              tip.style.top = e.clientY + $(window).scrollTop() + 25 + "px"
            })
            break
          case "bottom-left":
            $("body").on(event, function (e) {
              window.m = mouseCoords(e)
              tip.style.left = e.clientX + $(window).scrollLeft() - tip.offsetWidth + 10 + "px"
              tip.style.top = e.clientY + $(window).scrollTop() + 25 + "px"
            })
            break
          case "bottom":
            $("body").on(event, function (e) {
              window.m = mouseCoords(e)
              tip.style.left = e.clientX + $(window).scrollLeft() - tip.offsetWidth / 2 + "px"
              tip.style.top = e.clientY + $(window).scrollTop() + 25 + "px"
            })
            break
          case "top":
            //_data.mapTip.css('transform','translateX(-50%)');
            $("body").on(event, function (e) {
              window.m = mouseCoords(e)
              tip.style.left = e.clientX + $(window).scrollLeft() - tip.offsetWidth / 2 + "px"
              tip.style.top = e.clientY + $(window).scrollTop() - tip.offsetHeight - 10 + "px"
            })
            break
          case "top-right":
            $("body").on(event, function (e) {
              window.m = mouseCoords(e)
              tip.style.left = e.clientX + $(window).scrollLeft() + "px"
              tip.style.top = e.clientY + $(window).scrollTop() - tip.offsetHeight - 10 + "px"
            })
            break
          case "top-left":
            $("body").on(event, function (e) {
              window.m = mouseCoords(e)
              tip.style.left = e.clientX + $(window).scrollLeft() - tip.offsetWidth + 10 + "px"
              tip.style.top = e.clientY + $(window).scrollTop() - tip.offsetHeight - 10 + "px"
            })
            break
          case "left":
            $("body").on(event, function (e) {
              window.m = mouseCoords(e)
              tip.style.left = e.clientX + $(window).scrollLeft() - tip.offsetWidth - 10 + "px"
              tip.style.top = e.clientY + $(window).scrollTop() - tip.offsetHeight / 2 + "px"
            })
            break
          case "right":
            $("body").on(event, function (e) {
              window.m = mouseCoords(e)
              tip.style.left = e.clientX + $(window).scrollLeft() + 15 + "px"
              tip.style.top = e.clientY + $(window).scrollTop() - tip.offsetHeight / 2 + "px"
            })
            break
        }
      },
      setPopovers: function (options) {
        if (typeof options.mode == "string" && options.mode.indexOf("function") == 0) {
          options.mode = _this.functionFromString(options.mode)
        }

        $.extend(true, _data.options, { popovers: options })
        _data.options.popovers.on = _data.options.popovers.mode != "off"

        if (!_data.mapPopover) {
          _data.mapPopover = $("<div />").addClass("mapsvg-popover")
          _data.mapPopover.closeButton = $('<div class="mapsvg-popover-close">&#10005;</div>')
          _data.mapPopover.contentDiv = $('<div class="mapsvg-popover-content"></div>')
          _data.mapPopover.append(_data.mapPopover.contentDiv)
          _data.mapPopover.append(_data.mapPopover.closeButton)
          _data.mapPopover.css({
            width:
              _data.options.popovers.width + (_data.options.popovers.width == "auto" ? "" : "px"),
            height:
              _data.options.popovers.height + (_data.options.popovers.height == "auto" ? "" : "px"),
          })
          _data.mapPopover.closeButton.on("click", _this.hidePopover)
          $("body").append(_data.mapPopover)
        }

        $("body").off(".popover.mapsvg", _this.popoverOffHandler)
        if (!touchDevice) $("body").on("mousedown.popover.mapsvg", _this.popoverOffHandler)
      },
      setInitialViewBox: function (v) {
        if (typeof v == "string") v = v.trim().split(" ")
        // TODO is this OK?
        _data._viewBox = [parseFloat(v[0]), parseFloat(v[1]), parseFloat(v[2]), parseFloat(v[3])]
        _data.zoomLevel = 0
      },
      setViewBox: function (v, skipAdjustments) {
        if (typeof v == "string") {
          v = v.trim().split(" ")
          //var coords = _this.getRegion(v).getBBox();
          //_data.viewBox = [coords.x-5, coords.y-5, coords.width+10, coords.height+10];
          //var isZooming = true;
        }
        var d = v && v.length == 4 ? v : _data.svgDefault.viewBox
        var isZooming = parseInt(d[2]) != _data.viewBox[2] || parseInt(d[3]) != _data.viewBox[3]
        _data.viewBox = [parseFloat(d[0]), parseFloat(d[1]), parseFloat(d[2]), parseFloat(d[3])]

        if (!v) {
          _data._viewBox = _data.viewBox
          _data._scale = 1
        }
        _data.$svg[0].setAttribute("viewBox", _data.viewBox.join(" "))

        _this.setResponsive(_data.options.responsive, true)
        if (isZooming && !skipAdjustments) {
          _data.scale = _this.getScale()
          _this.markersAdjustPosition()
          if (_browser.ie) {
            // || _browser.firefox){
            _this.mapAdjustStrokes()
          }
        }
        return true
      },
      redraw: function () {
        _data.$map.css({
          width: _data.$map.width(),
          height: _data.$map.width() / _data.whRatio,
        })
        if (!_browser.ie) {
          _data.$map.css({
            width: "auto",
            height: "auto",
          })
        } else {
          _data.$map.css({
            width: "auto",
          })
        }
        _this.updateSize()
      },
      setSize: function (width, height, responsive) {
        // Convert strings to numbers
        _data.options.width = parseFloat(width)
        _data.options.height = parseFloat(height)
        _data.options.responsive =
          responsive != null ? parseBoolean(responsive) : _data.options.responsive

        // Calculate width and height
        if (!_data.options.width && !_data.options.height) {
          _data.options.width = _data.svgDefault.width
          _data.options.height = _data.svgDefault.height
        } else if (!_data.options.width && _data.options.height) {
          _data.options.width = parseInt(
            (_data.options.height * _data.svgDefault.width) / _data.svgDefault.height,
          )
        } else if (_data.options.width && !_data.options.height) {
          _data.options.height = parseInt(
            (_data.options.width * _data.svgDefault.height) / _data.svgDefault.width,
          )
        }

        //if(_data.options.responsive){
        //    var maxWidth  = _data.options.width;
        //    var maxHeight = _data.options.height;
        //    _data.options.width	 = _data.svgDefault.width;
        //    _data.options.height = _data.svgDefault.height;
        //}

        _data.whRatio = _data.options.width / _data.options.height
        _data.scale = _this.getScale()

        _this.setResponsive(responsive)

        if (_data.markers) _this.markersAdjustPosition()

        return [_data.options.width, _data.options.height]
      },
      setResponsive: function (on, force) {
        //if(parseBoolean(on)==_data.options.responsive)
        //    return false;
        on = on != undefined ? parseBoolean(on) : _data.options.responsive
        $(window).off("resize.mapsvg")

        if (on) {
          $(window).on("resize.mapsvg", _this.updateSize)
          if (!_data.$map.hasClass("mapsvg-responsive")) {
            _data.$map.addClass("mapsvg-responsive")
            _data.$map.css({
              width: "auto",
              height: "auto",
            })
          }

          _data.$map.height(_data.$map.width() / _data.whRatio)
          $(window).on("resize.mapsvg", function () {
            _data.$map.height(_data.$map.width() / _data.whRatio)
            _this.updateSize()
          })
        } else {
          _data.$map.removeClass("mapsvg-responsive")
          _data.$map.css({
            width: _data.options.width + "px",
            height: _data.options.height + "px",
          })
        }
        $.extend(true, _data.options, { responsive: on })
        _this.updateSize()
      },
      setScroll: function (options) {
        options.on != null && (options.on = parseBoolean(options.on))
        options.limit != null && (options.limit = parseBoolean(options.limit))
        $.extend(true, _data.options, { scroll: options })
        _this.setEventHandlers()
      },
      setZoom: function (options) {
        options = options || {}
        options.on != undefined && (options.on = parseBoolean(options.on))
        options.mousewheel != undefined && (options.mousewheel = parseBoolean(options.mousewheel))
        options.delta && (options.delta = parseFloat(options.delta))
        if (options.limit) {
          if (typeof options.limit == "string") options.limit = options.limit.split(";")
          options.limit = [parseInt(options.limit[0]), parseInt(options.limit[1])]
        }
        //(options.buttons && options.buttons.on) && (options.buttons.on = parseBoolean(options.buttons.on));
        $.extend(true, _data.options, { zoom: options })
        _data.$map.off("mousewheel.mapsvg")

        if (_data.options.zoom.on && _data.options.zoom.mousewheel) {
          _data.$map.on("mousewheel.mapsvg", function (event, delta, deltaX, deltaY) {
            var d = delta > 0 ? 1 : -1
            _this.zoom(d)
            return false
          })
        }
        _this.setZoomButtons()
      },
      setZoomButtons: function () {
        var loc = _data.options.zoom.buttons.location || "hide"
        if (!_data.zoomButtons) {
          var buttons = $("<div />").addClass("mapsvg-buttons")

          buttons.zoomIn = $("<div />").addClass("mapsvg-btn-zoom in")
          var event = touchDevice ? "touchstart" : "click"
          buttons.zoomIn.on(event, function (e) {
            e.stopPropagation()
            _this.zoomIn()
          })

          buttons.zoomOut = $("<div />").addClass("mapsvg-btn-zoom out")
          buttons.zoomOut.on(event, function (e) {
            e.stopPropagation()
            _this.zoomOut()
          })
          buttons.append(buttons.zoomIn).append(buttons.zoomOut)
          _data.zoomButtons = buttons
          _data.$map.append(_data.zoomButtons)
        }
        _data.zoomButtons.removeClass("left")
        _data.zoomButtons.removeClass("right")
        ;(loc == "right" && _data.zoomButtons.addClass("right")) ||
          (loc == "left" && _data.zoomButtons.addClass("left"))

        _data.options.zoom.on && loc != "hide" ? _data.zoomButtons.show() : _data.zoomButtons.hide()
      },
      setCursor: function (type) {
        type = type == "pointer" ? "pointer" : "default"
        _data.options.cursor = type
        if (type == "pointer") _data.$map.addClass("mapsvg-cursor-pointer")
        else _data.$map.removeClass("mapsvg-cursor-pointer")
      },
      setPreloaderText: function (text) {
        _data.options.loadingText = text
      },
      setMultiSelect: function (on) {
        _data.options.multiSelect = parseBoolean(on)
        _this.deselectAllRegions()
      },
      setGauge: function (options) {
        options = options || _data.options.gauge
        options.on != undefined && (options.on = parseBoolean(options.on))
        $.extend(true, _data.options, { gauge: options })

        var needsRedraw = false

        if (!_data.$gauge) {
          _data.$gauge = {}
          _data.$gauge.gradient = $("<td>&nbsp;</td>").addClass("mapsvg-gauge-gradient")
          _this.setGaugeGradientCSS()
          _data.$gauge.container = $("<div />").addClass("mapsvg-gauge").hide()
          _data.$gauge.table = $("<table />")
          var tr = $("<tr />")
          _data.$gauge.labelLow = $("<td>" + _data.options.gauge.labels.low + "</td>")
          _data.$gauge.labelHigh = $("<td>" + _data.options.gauge.labels.high + "</td>")
          tr.append(_data.$gauge.labelLow)
          tr.append(_data.$gauge.gradient)
          tr.append(_data.$gauge.labelHigh)
          _data.$gauge.table.append(tr)
          _data.$gauge.container.append(_data.$gauge.table)
          _data.$map.append(_data.$gauge.container)
        }

        if (!_data.options.gauge.on && _data.$gauge.container.is(":visible")) {
          _data.$gauge.container.hide()
          needsRedraw = true
        } else if (_data.options.gauge.on && !_data.$gauge.container.is(":visible")) {
          _data.$gauge.container.show()
          needsRedraw = true
        }

        if (options.colors) {
          _data.options.gauge.colors.lowRGB = tinycolor(_data.options.gauge.colors.low).toRgb()
          _data.options.gauge.colors.highRGB = tinycolor(_data.options.gauge.colors.high).toRgb()
          _data.options.gauge.colors.diffRGB = {
            r: _data.options.gauge.colors.highRGB.r - _data.options.gauge.colors.lowRGB.r,
            g: _data.options.gauge.colors.highRGB.g - _data.options.gauge.colors.lowRGB.g,
            b: _data.options.gauge.colors.highRGB.b - _data.options.gauge.colors.lowRGB.b,
            a: _data.options.gauge.colors.highRGB.a - _data.options.gauge.colors.lowRGB.a,
          }
          needsRedraw = true
          _data.$gauge && _this.setGaugeGradientCSS()
        }

        if (options.labels) {
          _data.$gauge.labelLow.html(_data.options.gauge.labels.low)
          _data.$gauge.labelHigh.html(_data.options.gauge.labels.high)
        }

        needsRedraw && _this.redrawGauge()
      },
      redrawGauge: function () {
        _this.updateGaugeMinMax()
        _this.regionsRedrawColors()
      },
      updateGaugeMinMax: function () {
        _data.options.gauge.min = 0
        _data.options.gauge.max = false
        var values = []
        _data.regions.forEach(function (r) {
          if (r.gaugeValue != null && r.gaugeValue != undefined) values.push(r.gaugeValue)
        })
        if (values.length > 0) {
          _data.options.gauge.min = values.length == 1 ? 0 : Math.min.apply(null, values)
          _data.options.gauge.max = Math.max.apply(null, values)
          _data.options.gauge.maxAdjusted = _data.options.gauge.max - _data.options.gauge.min
        }
      },
      setGaugeGradientCSS: function () {
        _data.$gauge.gradient.css({
          background: _data.options.gauge.colors.low,
          background:
            "-moz-linear-gradient(left, " +
            _data.options.gauge.colors.low +
            " 1%," +
            _data.options.gauge.colors.high +
            " 100%)",
          background:
            "-webkit-gradient(linear, left top, right top, color-stop(1%," +
            _data.options.gauge.colors.low +
            "), color-stop(100%," +
            _data.options.gauge.colors.high +
            "))",
          background:
            "-webkit-linear-gradient(left, " +
            _data.options.gauge.colors.low +
            " 1%," +
            _data.options.gauge.colors.high +
            " 100%)",
          background:
            "-o-linear-gradient(left, " +
            _data.options.gauge.colors.low +
            " 1%," +
            _data.options.gauge.colors.high +
            " 100% 100%)",
          background:
            "-ms-linear-gradient(left,  " +
            _data.options.gauge.colors.low +
            " 1%," +
            _data.options.gauge.colors.high +
            " 100% 100%)",
          background:
            "linear-gradient(to right," +
            _data.options.gauge.colors.low +
            " 1%," +
            _data.options.gauge.colors.high +
            " 100%)",
          filter:
            'progid:DXImageTransform.Microsoft.gradient( startColorstr="' +
            _data.options.gauge.colors.low +
            '", endColorstr="' +
            _data.options.gauge.colors.high +
            '",GradientType=1 )',
        })
      },
      setMenu: function (options) {
        options = options || _data.options.menu
        options.on != undefined && (options.on = parseBoolean(options.on))
        $.extend(true, _data.options, { menu: options })

        _data.$menu && _data.$menu.off("click.menu.mapsvg")

        if (_data.options.menu.on) {
          var menuContainer = $("#" + _data.options.menu.containerId)

          if (menuContainer.length) {
            if (!_data.$menu) {
              if (!menuContainer.is("ul")) {
                _data.$menu = $("<ul />").appendTo(menuContainer)
              } else {
                _data.$menu = menuContainer
              }

              if (!_data.$menu.hasClass("mapsvg-menu")) _data.$menu.addClass("mapsvg-menu")
            }

            if (_data.$menu.children().length === 0)
              // Add links into navigation container
              _data.regions.forEach(function (region, i) {
                if (!region.disabled) _data.$menu.append(_data.options.menu.template(region))
              })

            _data.$menu
              .on("click.menu.mapsvg", "a", function (e) {
                e.preventDefault()
                var regionID = $(this).attr("href").replace("#", "")
                var region = _this.getRegion(regionID)
                var center = region.getCenter()
                //e = {clientX: center[0], clientY: center[1]};
                e.clientX = center[0]
                e.clientY = center[1]

                _this.regionClickHandler(e, region)
              })
              .on("mouseover.menu.mapsvg", "a", function (e) {
                var regionID = $(this).attr("href").replace("#", "")
                var region = _this.getRegion(regionID)
                if (!region.selected) region.highlight()
              })
              .on("mouseout.menu.mapsvg", "a", function (e) {
                var regionID = $(this).attr("href").replace("#", "")
                var region = _this.getRegion(regionID)
                if (!region.selected) region.unhighlight()
              })
          }
        }
      },
      setMenuMarkers: function (options) {
        options = options || _data.options.menuMarkers
        options.on != undefined && (options.on = parseBoolean(options.on))
        $.extend(true, _data.options, { menuMarkers: options })

        _data.$menuMarkers && _data.$menuMarkers.off("click.menuMarkers.mapsvg")

        if (_data.options.menuMarkers.on) {
          var menuContainer = $("#" + _data.options.menuMarkers.containerId)
          if (menuContainer.length) {
            if (!_data.$menuMarkers) {
              if (!menuContainer.is("ul")) {
                _data.$menuMarkers = $("<ul />").appendTo(menuContainer)
              } else {
                _data.$menuMarkers = menuContainer
              }

              if (!_data.$menuMarkers.hasClass("mapsvg-menu-markers"))
                _data.$menuMarkers.addClass("mapsvg-menu-markers")
            }

            if (_data.$menuMarkers.children().length === 0)
              // Add links into navigation container
              _data.markers.forEach(function (marker, i) {
                _data.$menuMarkers.append(_data.options.menuMarkers.template(marker))
              })

            _data.$menuMarkers.on("click.menuMarkers.mapsvg", "a", function (e) {
              e.preventDefault()
              var markerID = $(this).attr("href").replace("#", "")
              var marker = _this.getMarker(markerID)
              var center = marker.getCenter()
              e = { clientX: center[0], clientY: center[1] }
              _this.regionClickHandler(e, marker)
            })
          }
        }
      },
      /*
       *
       * END SETTERS
       *
       * */
      getRegion: function (id) {
        return _data.regions[_data.regionsDict[id]]
      },
      getMarker: function (id) {
        return _data.markers[_data.markersDict[id]]
      },
      checkId: function (id) {
        if (_this.getRegion(id)) return { error: "This ID is already being used by a Region" }
        else if (_this.getMarker(id))
          return { error: "This ID is already being used by another Marker" }
        else return true
      },
      regionsRedrawColors: function () {
        _data.regions.forEach(function (region) {
          region.setFill()
        })
      },
      // destroy
      destroy: function () {
        delete instances[_data.$map.attr("id")]
        _data.$map.empty()
        return _this
      },
      getData: function () {
        return _data
      },
      // GET SCALE VALUE
      getScale: function () {
        var ratio_def = _data.svgDefault.width / _data.svgDefault.height
        var ratio_new = _data.options.width / _data.options.height
        var scale1, scale2

        var size = [_data.$map.width(), _data.$map.height()]

        scale2 = size[0] / _data.viewBox[2]

        // if scale = 0 it means that map width = 0 which means that map is hidden.
        // so we set scale = 1 to avoid problems with marker positioning.
        // proper scale will be set after map show up
        return scale2 || 1
      },
      updateSize: function () {
        _data.scale = _this.getScale()
        _this.markersAdjustPosition()
        if (_browser.ie) {
          _this.mapAdjustStrokes()
        }
      },
      // GET VIEBOX [x,y,width,height]
      getViewBox: function () {
        return _data.viewBox
      },
      // SET VIEWBOX BY SIZE
      viewBoxSetBySize: function (width, height) {
        _data._viewBox = _this.viewBoxGetBySize(width, height)
        _data.options.width = parseFloat(width)
        _data.options.height = parseFloat(height)

        //var v = _this.viewBoxGetBySize(width,height);
        //_data.viewBox  = $.extend([],_data._viewBox);
        //_data.scale    = _this.getScale();
        //_data.zoomLevel = 0;

        _this.setViewBox(_data._viewBox, true)

        //_data.$svgStretcher.attr('src',
        //    "data:image/svg+xml;charset=utf-8,<svg viewBox='0 0 "+_data._viewBox[2]+" "+_data._viewBox[3]+"' xmlns='http://www.w3.org/2000/svg'/>"
        //);
        //

        _data.whRatio = _data.viewBox[2] / _data.viewBox[3]
        if (!_data.options.responsive) _this.setResponsive()
        //_this.updateSize();

        return _data.viewBox
      },
      viewBoxGetBySize: function (width, height) {
        var new_ratio = width / height
        var old_ratio = _data.svgDefault.viewBox[2] / _data.svgDefault.viewBox[3]

        var vb = $.extend([], _data.svgDefault.viewBox)

        if (new_ratio != old_ratio) {
          //vb[2] = width*_data.svgDefault.viewBox[2] / _data.svgDefault.width;
          //vb[3] = height*_data.svgDefault.viewBox[3] / _data.svgDefault.height;
          if (new_ratio > old_ratio) {
            vb[2] = _data.svgDefault.viewBox[3] * new_ratio
            vb[0] = _data.svgDefault.viewBox[0] - (vb[2] - _data.svgDefault.viewBox[2]) / 2
          } else {
            vb[3] = _data.svgDefault.viewBox[2] / new_ratio
            vb[1] = _data.svgDefault.viewBox[1] - (vb[3] - _data.svgDefault.viewBox[3]) / 2
          }
        }

        return vb
      },
      viewBoxReset: function (toInitial) {
        if (toInitial) {
          var v = _data._viewBox || _data.svgDefault.viewBox
          _data.zoomLevel = 0
          _data._scale = 1
          _this.setViewBox(v)
        } else {
          _this.setViewBox()
        }
      },
      getGeoViewBox: function () {
        var v = _data.viewBox
        var leftLon = _this.convertPixelToGeo(v[0], v[1])[1]
        var rightLon = _this.convertPixelToGeo(v[0] + v[2], v[1])[1]
        var topLat = _this.convertPixelToGeo(v[0], v[1])[0]
        var bottomLat = _this.convertPixelToGeo(v[0], v[1] + v[3])[0]
        return [leftLon, topLat, rightLon, bottomLat]
      },
      mapAdjustStrokes: function () {
        _data.regions.forEach(function (region) {
          if (region.svg_style["stroke-width"]) {
            region.node.css("stroke-width", region.svg_style["stroke-width"] / _data.scale)
          }
        })
      },
      // ZOOM
      zoomIn: function () {
        _this.zoom(1)
      },
      zoomOut: function () {
        _this.zoom(-1)
      },
      touchZoomStart: function (touchScale) {
        touchZoomStart = _data._scale
        _data.scale = _data.scale * zoom_k
        zoom = _data._scale
        _data._scale = _data._scale * zoom_k

        var vWidth = _data.viewBox[2]
        var vHeight = _data.viewBox[3]
        var newViewBox = []

        newViewBox[2] = _data._viewBox[2] / _data._scale
        newViewBox[3] = _data._viewBox[3] / _data._scale

        newViewBox[0] = _data.viewBox[0] + (vWidth - newViewBox[2]) / 2
        newViewBox[1] = viewBox[1] + (vHeight - newViewBox[3]) / 2

        _this.setViewBox(newViewBox)
      },
      touchZoomMove: function () {},
      touchZoomEnd: function () {},
      zoom: function (delta, exact) {
        var vWidth = _data.viewBox[2]
        var vHeight = _data.viewBox[3]
        var newViewBox = []

        if (!exact) {
          // check for zoom limit
          var d = delta > 0 ? 1 : -1
          _data._zoomLevel = _data.zoomLevel
          _data._zoomLevel += d
          if (
            _data._zoomLevel > _data.options.zoom.limit[1] ||
            _data._zoomLevel < _data.options.zoom.limit[0]
          )
            return false

          _data.zoomLevel = _data._zoomLevel

          var zoom_k = d * _data.options.zoom.delta
          if (zoom_k < 1) zoom_k = -1 / zoom_k

          _data._scale = _data._scale * zoom_k
          newViewBox[2] = _data._viewBox[2] / _data._scale
          newViewBox[3] = _data._viewBox[3] / _data._scale
        } else {
          _data._scale = exact
          newViewBox[2] = _data.touchZoomStartViewBox[2] / _data._scale
          newViewBox[3] = _data.touchZoomStartViewBox[3] / _data._scale
        }

        newViewBox[0] = _data.viewBox[0] + (vWidth - newViewBox[2]) / 2
        newViewBox[1] = _data.viewBox[1] + (vHeight - newViewBox[3]) / 2
        // Limit scroll to map's boundaries
        if (_data.options.scroll.limit) {
          if (newViewBox[0] < _data.svgDefault.viewBox[0])
            newViewBox[0] = _data.svgDefault.viewBox[0]
          else if (
            newViewBox[0] + newViewBox[2] >
            _data.svgDefault.viewBox[0] + _data.svgDefault.viewBox[2]
          )
            newViewBox[0] =
              _data.svgDefault.viewBox[0] + _data.svgDefault.viewBox[2] - newViewBox[2]

          if (newViewBox[1] < _data.svgDefault.viewBox[1])
            newViewBox[1] = _data.svgDefault.viewBox[1]
          else if (
            newViewBox[1] + newViewBox[3] >
            _data.svgDefault.viewBox[1] + _data.svgDefault.viewBox[3]
          )
            newViewBox[1] =
              _data.svgDefault.viewBox[1] + _data.svgDefault.viewBox[3] - newViewBox[3]
        }

        _this.setViewBox(newViewBox)
      },
      // MARK : DELETE
      markerDelete: function (marker) {
        var id = marker.id
        var test = _data.markers.splice(_data.markersDict[id], 1)
        //_data.options.markers.splice(_data.markersDict[id],1);
        marker.delete()
        marker = null
        _this.updateMarkersDict()
        if (_data.markers.length == 0) _data.options.markerLastID = 0
      },
      // MARK : ADD
      markerAdd: function (opts, create) {
        // Join default marker options with user-defined options
        var options = $.extend(true, {}, markerOptions, opts)

        if (!options.src) return false

        options.src = safeURL(options.src)

        if (options.width && options.height) {
          return _this.markerAddFinalStep(options, create)
        } else {
          var img = new Image()
          img.onload = function () {
            options.width = this.width
            options.height = this.height
            return _this.markerAddFinalStep(options, create)
          }
          img.src = options.src
        }
      },
      markerAddFinalStep: function (options, create) {
        if (options.xy || (isNumber(options.x) && isNumber(options.y))) {
          xy = options.xy || [options.x, options.y]
          if (create) {
            xy[0] += _data.scale < 1 ? 1 : 2 // not sure what's happening here but works good
            xy[1] += 1

            //xy[0] = xy[0]/_data.scale - options.width/(2*_data.scale) + _data.viewBox[0];
            //xy[1] = (xy[1]-options.height)/_data.scale + _data.viewBox[1];
            //
            //markerX       = markerX + markerWidth/(2*mapScale) - markerWidth/2;
            //markerY       = markerY + markerHeight/mapScale - markerHeight;

            xy[0] = xy[0] / _data.scale - options.width / 2 + _data.viewBox[0]
            xy[1] = xy[1] / _data.scale - options.height + _data.viewBox[1]

            //xy = Marker.getDefaultCoords(xy[0],xy[1], options.width, options.height, this.getScale());
          }
        } else if (options.geoCoords) {
          xy = _this.convertGeoToPixel(options.geoCoords)
          xy[0] = xy[0] - options.width / 2 + 0.3
          xy[1] = xy[1] - options.height + 1.5
        } else {
          return false
        }

        options.x = xy[0]
        options.y = xy[1]
        options.id = options.id || "marker_" + _data.options.markerLastID++
        if (!options.geoCoords && _data.mapIsGeo) {
          options.geoCoords = _this.convertPixelToGeo(
            options.x + options.width / 2,
            options.y + (options.height - 1),
          )
        }

        var marker = new Marker(options, _data.scale)

        _data.$svg.append(marker.node)
        marker.href && marker.setHref(marker.href)
        // Create a group
        // var group = marker.data('group') || 'root';
        //                if(!_data.markerGroups[group])
        //                    _data.markerGroups[group] = [];
        //                _data.markerGroups[group].push(marker);

        marker.adjustPosition(_data.scale)
        _data.markers.push(marker)
        _data.markersDict[marker.id] = _data.markers.length - 1

        // Call edit window
        if (create && _data.markerEditHandler) _data.markerEditHandler.call(marker)

        return marker
      },
      markersAdjustPosition: function () {
        // We want a marker "tip" to be on bottom side (like a pin)
        // But Raphael starts to draw an image from left top corner.
        // At the same time we don't want a marker to be scaled in size when map scales;
        // Mark always should stay the same size.
        // In this case coordinates of bottom point of image will vary with map scaling.
        // So we have to calculate the offset.
        var dx, dy
        _data.markers.forEach(function (marker) {
          marker.adjustPosition(_data.scale)
        })
      },
      // MARK MOVE & EDIT HANDLERS
      markerMoveStart: function () {
        // storing original coordinates
        this.data("ox", parseFloat(this.attr("x")))
        this.data("oy", parseFloat(this.attr("y")))
      },
      markerMove: function (dx, dy) {
        dx = dx / _data.scale
        dy = dy / _data.scale
        this.attr({ x: this.data("ox") + dx, y: this.data("oy") + dy })
      },
      markerMoveEnd: function () {
        // if coordinates are same then it was a "click" and we should start editing
        if (this.data("ox") == this.attr("x") && this.data("oy") == this.attr("y")) {
          options.markerEditHandler.call(this)
        }
      },
      scrollStart: function (e, mapsvg) {
        if ($(e.target).hasClass("mapsvg-btn-zoom") || $(e.target).closest(".mapsvg-gauge").length)
          return false

        if (_data.editMarkers.on && $(e.target).attr("class") == "mapsvg-marker") return false
        //mapsvg.getData().$map.css('pointer-events','none');

        e.preventDefault()
        if (touchDevice) {
          var ce =
            e.originalEvent && e.originalEvent.touches && e.originalEvent.touches[0]
              ? e.originalEvent.touches[0]
              : e
        } else {
          var ce = e
        }

        _data.scroll = {}

        // initial viewbox when scrollning started
        _data.scroll.vxi = _data.viewBox[0]
        _data.scroll.vyi = _data.viewBox[1]
        // mouse coordinates when scrollning started
        _data.scroll.x = ce.clientX
        _data.scroll.y = ce.clientY
        // mouse delta
        _data.scroll.dx = 0
        _data.scroll.dy = 0
        // new viewbox x/y
        _data.scroll.vx = 0
        _data.scroll.vy = 0

        if (!touchDevice)
          $("body")
            .on("mousemove.scroll.mapsvg", _this.scrollMove)
            .on("mouseup.scroll.mapsvg", function (e) {
              _this.scrollEnd(e, mapsvg)
            })
        //else
        //    $('body').on('touchmove.scroll.mapsvg', _this.scrollMove).on('touchmove.scroll.mapsvg', function(e){_this.scrollEnd(e,mapsvg);});
      },
      scrollMove: function (e) {
        e.preventDefault()

        _data.isScrolling = true

        $("body").css({ cursor: "move" })

        var ce =
          e.originalEvent && e.originalEvent.touches && e.originalEvent.touches[0]
            ? e.originalEvent.touches[0]
            : e

        // delta x/y
        _data.scroll.dx = _data.scroll.x - ce.clientX
        _data.scroll.dy = _data.scroll.y - ce.clientY

        // new viewBox x/y
        var vx = parseInt(_data.scroll.vxi + _data.scroll.dx / _data.scale)
        var vy = parseInt(_data.scroll.vyi + _data.scroll.dy / _data.scale)

        // Limit scroll to map's boundaries
        if (_data.options.scroll.limit) {
          if (vx < _data.svgDefault.viewBox[0]) vx = _data.svgDefault.viewBox[0]
          else if (
            _data.viewBox[2] + vx >
            _data.svgDefault.viewBox[0] + _data.svgDefault.viewBox[2]
          )
            vx = _data.svgDefault.viewBox[0] + _data.svgDefault.viewBox[2] - _data.viewBox[2]

          if (vy < _data.svgDefault.viewBox[1]) vy = _data.svgDefault.viewBox[1]
          else if (
            _data.viewBox[3] + vy >
            _data.svgDefault.viewBox[1] + _data.svgDefault.viewBox[3]
          )
            vy = _data.svgDefault.viewBox[1] + _data.svgDefault.viewBox[3] - _data.viewBox[3]
        }

        _data.scroll.vx = vx
        _data.scroll.vy = vy

        // set new viewBox
        _this.setViewBox([_data.scroll.vx, _data.scroll.vy, _data.viewBox[2], _data.viewBox[3]])
        //_data.$map.css({'-webkit-transform' : 'translate('+vx+'px,'+vy+'px'});
      },
      scrollEnd: function (e, mapsvg) {
        _data.isScrolling = false

        // call regionClickHandler if mouse did not move more than 5 pixels
        if (Math.abs(_data.scroll.dx) < 5 && Math.abs(_data.scroll.dy) < 5) {
          if (_data.editMarkers.on) _this.markerAddClickHandler(e)
          else if (_data.region_clicked) _this.regionClickHandler(e, _data.region_clicked)
        }

        $("body").css({ cursor: "default" })

        _data.viewBox[0] = _data.scroll.vx || _data.viewBox[0]
        _data.viewBox[1] = _data.scroll.vy || _data.viewBox[1]

        //_data.$map.css('pointer-events','auto');

        $("body").off(".scroll.mapsvg")
      },
      // REMEMBER WHICH REGION WAS CLICKED BEFORE START PANNING
      scrollRegionClickHandler: function (e, region) {
        _data.region_clicked = region
      },
      touchStart: function (_e, mapsvg) {
        _e.preventDefault()
        var e = _e.originalEvent
        if (_data.options.zoom && e.touches && e.touches.length == 2) {
          _data.touchZoomStartViewBox = _data.viewBox
          _data.touchZoomStart = _data.scale
          _data.touchZoomEnd = 1
        } else {
          _this.scrollStart(_e, mapsvg)
          _data.isScrolling = true
        }
      },
      touchMove: function (_e) {
        _e.preventDefault()
        var e = _e.originalEvent
        if (_data.options.zoom && e.touches && e.touches.length >= 2) {
          _this.zoom(null, e.scale)
          _data.isScrolling = false
        } else if (_data.isScrolling) {
          _this.scrollMove(_e)
        }
      },
      touchEnd: function (_e) {
        _e.preventDefault()
        var e = _e.originalEvent
        if (_data.touchZoomStart) {
          _data.touchZoomStart = false
          _data.touchZoomEnd = false
        } else if (_data.isScrolling) {
          _this.scrollEnd(_e)
        }
      },
      markersGroupHide: function (group) {
        for (var i in _data.markers[group]) {
          _data.markers[group][i].hide()
        }
      },
      markersGroupShow: function (group) {
        for (var i in _data.markers[group]) {
          _data.markers[group][i].show()
        }
      },
      regionsGroupSelect: function (group) {
        for (var i in _data.markers[group]) {
          _data.markers[group][i].hide()
        }
      },
      regionsGroupUnselect: function (group) {
        for (var i in _data.markers[group]) {
          _data.markers[group][i].show()
        }
      },
      // GET ALL MARKERS
      markersGet: function () {
        return _data.markers
      },
      // GET SELECTED REGION OR ARRAY OF SELECTED REGIONS
      getSelected: function () {
        return _data.selected_id
      },
      // SELECT REGION
      selectRegion: function (id) {
        if (typeof id == "string") {
          var region = _this.getRegion(id)
        } else {
          var region = id
        }
        if (!region || region.disabled) return false
        if (_data.options.multiSelect && !_data.editRegions.on) {
          if (region.selected) {
            _this.deselectRegion(region)
            return
          }
        } else if (_data.selected_id.length > 0) {
          _this.deselectRegion()
        }
        _data.selected_id.push(region.id)
        region.select()
      },
      deselectAllRegions: function () {
        $.each(_data.selected_id, function (index, id) {
          _this.deselectRegion(_this.getRegion(id))
        })
      },
      deselectRegion: function (region) {
        if (!region) region = _this.getRegion(_data.selected_id[0])
        region.deselect()
        var i = $.inArray(region.id, _data.selected_id)
        _data.selected_id.splice(i, 1)
        if (_browser.ie)
          //|| _browser.firefox)
          _this.mapAdjustStrokes()
      },
      selectRegionsGroup: function (group_id) {
        for (var r in _data.regionGroups[group_id]) {
          var region_id = _data.regionGroups[group_id][r].id
          _this.selectRegion(region_id)
        }
      },
      highlightRegionsGroup: function (group_id) {
        for (var r in _data.regionGroups[group_id]) {
          var region_id = _data.regionGroups[group_id][r].id
          _this.highlightRegion(region_id)
        }
      },
      unhighlightRegionsGroup: function (group_id) {
        for (var r in _data.regionGroups[group_id]) {
          var region_id = _data.regionGroups[group_id][r].id
          _this.unhighlightRegion(region_id)
        }
      },

      narrowToRegionsGroup: function (group_id) {
        _data.regions.forEach(function (r) {
          if (r.group != group_id && !r.disabled) {
            r.disabled = true
            r.disabledTemp = true
            $(r.node).addClass("mapsvg-disabled")
          } else {
            if (r.disabledTemp) {
              r.disabledTemp = false
              r.disabled = false
              $(r.node).removeClass("mapsvg-disabled")
            }
          }
        })
      },
      disableRegionsGroup: function (group_id) {},
      enableAllRegionsGroups: function () {
        _data.regions.forEach(function (r) {
          if (r.disabledTemp) {
            r.disabledTemp = false
            r.disabled = false
            $(r.node).removeClass("mapsvg-disabled")
          }
        })
      },
      convertGeoToPixel: function (coords) {
        var lat = parseFloat(coords[0])
        var lon = parseFloat(coords[1])
        var x = (lon - _data.geoViewBox.leftLon) * (_data.svgDefault.width / _data.mapLonDelta)

        var lat = (lat * 3.14159) / 180
        var worldMapWidth = ((_data.svgDefault.width / _data.mapLonDelta) * 360) / (2 * 3.14159)
        var mapOffsetY =
          (worldMapWidth / 2) *
          Math.log(
            (1 + Math.sin(_data.mapLatBottomDegree)) / (1 - Math.sin(_data.mapLatBottomDegree)),
          )
        var y =
          _data.svgDefault.height -
          ((worldMapWidth / 2) * Math.log((1 + Math.sin(lat)) / (1 - Math.sin(lat))) - mapOffsetY)

        x += _data.svgDefault.viewBox[0]
        y += _data.svgDefault.viewBox[1]

        return [x, y]
      },
      convertPixelToGeo: function (tx, ty) {
        tx -= _data.svgDefault.viewBox[0]
        ty -= _data.svgDefault.viewBox[1]
        /* called worldMapWidth in Raphael's Code, but I think that's the radius since it's the map width or circumference divided by 2*PI  */
        var worldMapRadius = ((_data.svgDefault.width / _data.mapLonDelta) * 360) / (2 * Math.PI)
        var mapOffsetY =
          (worldMapRadius / 2) *
          Math.log(
            (1 + Math.sin(_data.mapLatBottomDegree)) / (1 - Math.sin(_data.mapLatBottomDegree)),
          )
        var equatorY = _data.svgDefault.height + mapOffsetY
        var a = (equatorY - ty) / worldMapRadius
        var lat = (180 / Math.PI) * (2 * Math.atan(Math.exp(a)) - Math.PI / 2)
        var long = _data.geoViewBox.leftLon + (tx / _data.svgDefault.width) * _data.mapLonDelta

        return [lat, long]
      },
      // PICK COLOR FROM GRADIENT
      pickGaugeColor: function (gaugeValue) {
        var w = (gaugeValue - _data.options.gauge.min) / _data.options.gauge.maxAdjusted
        var rgb = [
          Math.round(
            _data.options.gauge.colors.diffRGB.r * w + _data.options.gauge.colors.lowRGB.r,
          ),
          Math.round(
            _data.options.gauge.colors.diffRGB.g * w + _data.options.gauge.colors.lowRGB.g,
          ),
          Math.round(
            _data.options.gauge.colors.diffRGB.b * w + _data.options.gauge.colors.lowRGB.b,
          ),
          Math.round(
            _data.options.gauge.colors.diffRGB.a * w + _data.options.gauge.colors.lowRGB.a,
          ),
        ]
        return rgb
      },
      // CHECK IF REGION IS DISABLED
      isRegionDisabled: function (id, svgfill) {
        if (
          _data.options.regions[id] &&
          (_data.options.regions[id].disabled || svgfill == "none")
        ) {
          return true
        } else if (
          (_data.options.regions[id] == undefined ||
            parseBoolean(_data.options.regions[id].disabled)) &&
          (_data.options.disableAll || svgfill == "none" || id == "labels" || id == "Labels")
        ) {
          return true
        } else {
          return false
        }
      },
      regionClickHandler: function (e, region) {
        if (!region || region.disabled) return false

        _data.region_clicked = null

        if (region.mapsvg_type == "region") _this.selectRegion(region.id)
        if (_data.editRegions.on) {
          _data.regionEditHandler.call(region)
          return
        }
        _this.hidePopover()
        var popover = _this.getPopoverBody(region)
        popover && _this.showPopover(e, popover)

        if (_data.options.onClick) _data.options.onClick.call(region, e, _this)

        if (region.href && !_data.disableLinks) window.location.href = region.href
      },
      fileExists: function (url) {
        if (url.substr(0, 4) == "data") return true
        var http = new XMLHttpRequest()
        http.open("HEAD", url, false)
        http.send()
        return http.status != 404
      },
      getStyle: function (elem, prop) {
        if (elem.currentStyle) {
          var res = elem.currentStyle.margin
        } else if (window.getComputedStyle) {
          if (window.getComputedStyle.getPropertyValue) {
            var res = window.getComputedStyle(elem, null).getPropertyValue(prop)
          } else {
            var res = window.getComputedStyle(elem)[prop]
          }
        }
        return res
      },
      search: function (str) {
        results = []
        str = str.toLowerCase()
        _data.regions.forEach(function (r) {
          if (
            r.id.toLowerCase().indexOf(str) === 0 ||
            (r.title && r.title.toLowerCase().indexOf(str) === 0)
          )
            results.push(r.id)
        })
        return results
      },
      searchMarkers: function (str) {
        results = []
        str = str.toLowerCase()
        _data.markers.forEach(function (m) {
          if (m.id.toLowerCase().indexOf(str) === 0) results.push(m.id)
        })
        return results
      },
      markerAddClickHandler: function (e) {
        // Don't add marker if marker was clicked
        if ($(e.target).is("image")) return false
        var mc = mouseCoords(e)
        var x = mc.x - _data.$map.offset().left
        var y = mc.y - _data.$map.offset().top

        if (!$.isNumeric(x) || !$.isNumeric(y)) return false

        _this.markerAdd({ xy: [x, y], group: _data.editMarkers.group }, true)
      },
      setMarkersEditMode: function (on) {
        _data.editMarkers.on = parseBoolean(on)
        _this.deselectAllRegions()
        _this.setEventHandlers()
      },
      setRegionsEditMode: function (on) {
        _data.editRegions.on = parseBoolean(on)
        _this.deselectAllRegions()
        _this.setEventHandlers()
      },
      // Adding markers
      setMarkers: function (markers) {
        $.each(markers, function (i, marker) {
          _this.markerAdd(marker)
        })
      },
      setEventHandler: function () {},
      textBr: function (text) {
        var htmls = []
        var lines = text.split(/\n/)
        var tmpDiv = jQuery(document.createElement("div"))
        for (var i = 0; i < lines.length; i++) {
          htmls.push(tmpDiv.text(lines[i]).html())
        }
        return htmls.join("<br />")
      },
      runUserFunction: function (func) {
        try {
          func()
        } catch (error) {
          console.log(
            "MapSVG user-defined function error: (line " + error.line + "): " + error.message,
          )
        }
      },
      showTooltip: function (region) {
        if (region.disabled) return false

        var tip
        if (_data.options.tooltips.priority == "global") {
          tip =
            (typeof _data.options.tooltips.mode == "function" &&
              _data.options.tooltips.mode.call(region, _data.mapTip, region, _this)) ||
            (_data.options.tooltips.mode != "off" && region[_data.options.tooltips.mode]) ||
            region.tooltip
        } else {
          tip =
            region.tooltip ||
            (typeof _data.options.tooltips.mode == "function" &&
              _data.options.tooltips.mode.call(region, _data.mapTip, region, _this)) ||
            (_data.options.tooltips.mode != "off" && region[_data.options.tooltips.mode])
        }
        if (tip) {
          _data.mapTip.html(tip)
          _data.mapTip.addClass("mapsvg-tooltip-visible")
        }
      },
      getPopoverBody: function (region) {
        var popover
        if (_data.options.popovers.priority == "global") {
          popover =
            typeof _data.options.popovers.mode == "function"
              ? _data.options.popovers.mode.call(region, _data.mapPopover, region, _this)
              : region.popover
        } else {
          popover =
            region.popover ||
            (typeof _data.options.popovers.mode == "function"
              ? _data.options.popovers.mode.call(region, _data.mapPopover, region, _this)
              : null)
        }

        return popover
      },
      showPopover: function (e, content, pos) {
        if (!pos || pos.length != 2) {
          var m = mouseCoords(e)
          var pos = [m.x, m.y]
        } else {
          var scale = _this.getScale()
          pos[0] = _data.$map.offset().left + pos[0] * scale
          pos[1] = _data.$map.offset().top + pos[1] * scale
        }

        if (content) {
          _data.mapPopover.contentDiv.html(content)
          var nx = pos[0] - _data.mapPopover.outerWidth(false) / 2
          var ny = pos[1] - _data.mapPopover.outerHeight(false) - 14
          if (nx < 0) nx = 0
          if (ny < 0) ny = 0

          //if(nx+_data.mapPopover.outerWidth(false) > $(window).scrollLeft() + $(window).width()) nx = ($(window).scrollLeft() + $(window).width()) - _data.mapPopover.outerWidth(false);
          //if(ny+_data.mapPopover.outerHeight(false) > $(window).scrollTop() + $(window).height()) ny = ($(window).scrollTop() + $(window).height()) - _data.mapPopover.outerHeight(false);
          //if(nx < $(window).scrollLeft()) nx = $(window).scrollLeft();
          //if(ny < $(window).scrollTop()) ny = $(window).scrollTop();

          _data.mapPopover.css("left", nx).css("top", ny)
          //_data.mapPopover.show();
          _data.mapPopover.addClass("mapsvg-popover-visible")
        } else {
          _data.mapPopover.removeClass("mapsvg-popover-visible")
        }
      },
      hidePopover: function () {
        _data.mapPopover.contentDiv.empty()
        //_data.mapPopover.hide(0,function(){
        //    $('body').off('mousedown.popover.mapsvg', _this.popoverOffHandler);
        //    if(_data.options.onPopoverClose)
        //        _data.options.onPopoverClose.call(_this);
        //});
        _data.mapPopover.removeClass("mapsvg-popover-visible")
        $("body").off("mousedown.popover.mapsvg", _this.popoverOffHandler)
        if (_data.options.onPopoverClose) _data.options.onPopoverClose.call(_this)
      }, // Hide tooltip
      hideTip: function () {
        _data.mapTip.removeClass("mapsvg-tooltip-visible")
        //_data.mapTip.html('');
      },
      popoverOffHandler: function (e) {
        // If clicked object was popover by itself, stop.
        if ($(e.target).closest(".mapsvg-popover").length) return false

        _this.hidePopover()
      },
      mouseOverHandler: function (e) {
        //if (this.disabled)
        //    return false;
        if (this instanceof Region) {
          if (!this.selected) this.highlight()
        }
        _this.showTooltip(this)
        //// TODO как-то убрать
        //if(!_data.editRegions.on && !_data.editMarkers.on)
        return _data.options.mouseOver && _data.options.mouseOver.call(this, e, _this)
      },
      mouseOutHandler: function (e) {
        //if (this.disabled)
        //    return false;
        if (this instanceof Region) {
          if (!this.selected) this.unhighlight()
        }
        _this.hideTip()
        //// todo remove
        //if(!_data.editRegions.on && !_data.editMarkers.on)
        return _data.options.mouseOut && _data.options.mouseOut.call(this, e, _this)
      },
      updateOptions: function (options) {
        $.extend(true, _data.options, options)
      },
      updateMarkersDict: function () {
        _data.markersDict = {}
        _data.markers.forEach(function (marker, i) {
          _data.markersDict[marker.id] = i
        })
      },
      setEventHandlers: function () {
        _data.$map.off(".common.mapsvg")

        if (_data.editMarkers.on) {
          var event2 = touchDevice ? "touchstart.common.mapsvg" : "mousedown.common.mapsvg"
          _data.$map.on(event2, ".mapsvg-marker", function (e) {
            e.originalEvent.preventDefault()
            var marker = _this.getMarker($(this).attr("id"))
            var startCoords = mouseCoords(e)
            marker.drag(
              startCoords,
              _data.scale,
              function () {
                if (_data.mapIsGeo) {
                  // TODO fix y-shift
                  this.geoCoords = _this.convertPixelToGeo(
                    this.x + this.width / 2,
                    this.y + (this.height - 1),
                  )
                }

                _data.markerEditHandler.call(this, true)
              },
              function () {
                _data.markerEditHandler.call(this)
              },
            )
          })
        }

        // REGIONS
        if (!touchDevice) {
          if (!_data.editMarkers.on) {
            _data.$map
              .on("mouseover.common.mapsvg", ".mapsvg-region", function (e) {
                var id = $(this).attr("id")
                _this.mouseOverHandler.call(_this.getRegion(id), e, _this, options)
              })
              .on("mouseleave.common.mapsvg", ".mapsvg-region", function (e) {
                var id = $(this).attr("id")
                _this.mouseOutHandler.call(_this.getRegion(id), e, _this, options)
              })
          }
          if (!_data.editRegions.on) {
            _data.$map
              .on("mouseover.common.mapsvg", ".mapsvg-marker", function (e) {
                var id = $(this).attr("id")
                _this.mouseOverHandler.call(_this.getMarker(id), e, _this, options)
              })
              .on("mouseleave.common.mapsvg", ".mapsvg-marker", function (e) {
                var id = $(this).attr("id")
                _this.mouseOutHandler.call(_this.getMarker(id), e, _this, options)
              })
          }
        }

        if (!_data.options.scroll.on) {
          var event = touchDevice ? "touchstart.common.mapsvg" : "click.common.mapsvg"
          if (!_data.editMarkers.on) {
            _data.$map.on(event, ".mapsvg-region", function (e) {
              _this.regionClickHandler.call(_this, e, _this.getRegion($(this).attr("id")))
            })
            _data.$map.on(event, ".mapsvg-marker", function (e) {
              _this.regionClickHandler.call(_this, e, _this.getMarker($(this).attr("id")))
            })
          } else {
            _data.$map.on(event, function (e) {
              _this.markerAddClickHandler(e)
            })
          }
        } else {
          var event = touchDevice ? "touchstart.common.mapsvg" : "mousedown.common.mapsvg"
          _data.$map.on(event, ".mapsvg-region", function (e) {
            e.preventDefault()
            var obj = _this.getRegion($(this).attr("id"))
            _this.scrollRegionClickHandler.call(_this, e, obj)
          })
          _data.$map.on(event, ".mapsvg-marker", function (e) {
            e.preventDefault()
            var obj = _this.getMarker($(this).attr("id"))
            _this.scrollRegionClickHandler.call(_this, e, obj)
          })
          if (!touchDevice) {
            _data.$map.on("mousedown.common.mapsvg", function (e) {
              _this.scrollStart(e, _this)
            })
          } else {
            _data.$map
              .on("touchstart.common.mapsvg", function (e) {
                _this.touchStart(e, _this)
              })
              .on("touchmove.common.mapsvg", _this.touchMove)
              .on("touchend.common.mapsvg", _this.touchEnd)
          }
        }
      },
      // INIT
      init: function (opts, elem) {
        if (!opts.source) {
          throw new Error("MapSVG: please provide SVG file source.")
          return false
        }

        if (opts.beforeLoad)
          try {
            opts.beforeLoad.call(_this)
          } catch (err) {}

        // cut domain to avoid cross-domain errors
        if (opts.source.indexOf("//") === 0)
          opts.source = opts.source.replace(/^\/\/[^\/]+/, "").replace("//", "/")
        else opts.source = opts.source.replace(/^.*:\/\/[^\/]+/, "").replace("//", "/")

        /** Setting _data **/
        _data = {}
        _data.options = $.extend(true, {}, defaults, opts)
        _data.editRegions = { on: false }
        _data.editMarkers = { on: false }
        _data.map = elem
        _data.$map = $(elem)
        _data.whRatio = 0
        _data.isScrolling = false
        _data.markerOptions = {}
        _data.svgDefault = {}
        _data.refLength = 0
        _data.scale = 1 // absolute scale
        _data._scale = 1 // relative scale starting from current zoom level
        _data.selected_id = []
        _data.mapData = {}
        _data.regions = []
        _data.regionsDict = {}
        _data.regionID = { id: 0 }
        _data.markers = []
        _data.markersDict = {}
        _data._viewBox = [] // initial viewBox
        _data.viewBox = [] // current viewBox
        _data.viewBoxZoom = []
        _data.viewBoxFind = undefined
        _data.zoomLevel = 0
        _data.scroll = {}
        ;(_data.geoCoordinates = false),
          (_data.geoViewBox = { leftLon: 0, topLat: 0, rightLon: 0, bottomLat: 0 }),
          // Set background
          _data.$map.addClass("mapsvg").css("background", _data.options.colors.background)

        var loading = $("<div>" + _data.options.loadingText + "</div>").addClass("mapsvg-loading")
        _data.$map.append(loading)
        loading.css({
          "margin-left": function () {
            return -($(this).outerWidth(false) / 2) + "px"
          },
          "margin-top": function () {
            return -($(this).outerHeight(false) / 2) + "px"
          },
        })

        // GET the map by ajax request
        $.ajax({
          url: _data.options.source,
          success: function (xmlData) {
            $data = $(xmlData)

            // Default width/height/viewBox from SVG
            var svgTag = $data.find("svg")
            _data.$svg = svgTag

            _data.svgDefault.width = svgTag.attr("width")
            _data.svgDefault.height = svgTag.attr("height")
            _data.svgDefault.viewBox = svgTag.attr("viewBox")

            if (_data.svgDefault.width && _data.svgDefault.height) {
              _data.svgDefault.width = parseFloat(_data.svgDefault.width.replace(/px/g, ""))
              _data.svgDefault.height = parseFloat(_data.svgDefault.height.replace(/px/g, ""))
              _data.svgDefault.viewBox = _data.svgDefault.viewBox
                ? _data.svgDefault.viewBox.split(" ")
                : [0, 0, _data.svgDefault.width, _data.svgDefault.height]
            } else if (_data.svgDefault.viewBox) {
              _data.svgDefault.viewBox = _data.svgDefault.viewBox.split(" ")
              _data.svgDefault.width = parseFloat(_data.svgDefault.viewBox[2])
              _data.svgDefault.height = parseFloat(_data.svgDefault.viewBox[3])
            } else {
              alert("MapSVG needs width/height or viewBox parameter to be present in SVG file.")
              return false
            }

            // Get geo-coordinates view  box from SVG file
            var geo = svgTag.attr("mapsvg:geoViewBox")
            if (geo) {
              geo = geo.split(" ")
              if (geo.length == 4) {
                _data.mapIsGeo = true
                _data.geoCoordinates = true
                var v = svgTag.attr("mapsvg:calibratedViewBox")

                _data.geoViewBox = {
                  leftLon: parseFloat(geo[0]),
                  topLat: parseFloat(geo[1]),
                  rightLon: parseFloat(geo[2]),
                  bottomLat: parseFloat(geo[3]),
                }
                _data.mapLonDelta = _data.geoViewBox.rightLon - _data.geoViewBox.leftLon
                _data.mapLatBottomDegree = (_data.geoViewBox.bottomLat * 3.14159) / 180
              }
            }

            $.each(_data.svgDefault.viewBox, function (i, v) {
              _data.svgDefault.viewBox[i] = parseInt(v)
            })

            _data._viewBox =
              (_data.options.viewBox.length == 4 && _data.options.viewBox) ||
              _data.svgDefault.viewBox

            $.each(_data._viewBox, function (i, v) {
              _data._viewBox[i] = parseFloat(v)
            })

            svgTag.attr("preserveAspectRatio", "xMidYMid meet")
            svgTag.attr("width", "100%")
            svgTag.attr("height", "100%")

            //// Adding moving sticky draggable image on background
            //if(_data.options.scrollBackground)
            //    _data.background = _data.R.rect(_data.svgDefault.viewBox[0],_data.svgDefault.viewBox[1],_data.svgDefault.viewBox[2],_data.svgDefault.viewBox[3]).attr({fill: _data.options.colors.background});

            _data.$svg.find("path, polygon, circle, ellipse, rect").each(function (index) {
              var region = new Region($(this), _data.options, _data.regionID, _this)
              _data.regions.push(region)
              _data.regionsDict[region.id] = index
              if (region.autoID) {
                _data.presentAutoID = true
              }
            })

            // Set size
            _this.setSize(_data.options.width, _data.options.height, _data.options.responsive)

            _data.$map.append(svgTag)

            // Set viewBox
            var v = _data._viewBox
            _this.setViewBox(v)

            // SET Gauge colors
            _this.setGauge()

            // If there are markers, put them to the map
            var markers = _data.options.markers || _data.options.marks || []
            _this.setMarkers(markers)
            //_this.setMarkersEditMode(_data.editMarkers.on);

            // Set scrollning
            _this.setScroll(_data.options.scroll)

            // Set zooming by mouswheel
            _this.setZoom(_data.options.zoom)

            // Set tooltips
            // tooltipsMode is deprecated, need this for backward compatibility
            if (_data.options.tooltipsMode) _data.options.tooltips.mode = _data.options.tooltipsMode
            _this.setTooltips(_data.options.tooltips)

            // Set popovers
            // popover is deprecated (now it's popover), need this for backward compatibility
            if (_data.options.popover) _data.options.popovers = _data.options.popover
            _this.setPopovers(_data.options.popovers)

            if (_data.options.cursor) _this.setCursor(_data.options.cursor)

            if (_browser.ie)
              //|| _browser.firefox)
              _this.mapAdjustStrokes()

            $(document).ready(function () {
              _this.setMenu()
              _this.setMenuMarkers()
            })

            /* EVENTS */

            _this.setEventHandlers()

            $("#" + _data.$map.attr("id") + " [title]").each(function () {
              this.removeAttribute("title")
            })
            $("#" + _data.$map.attr("id") + " title").remove()

            loading.hide()

            if (_data.options.afterLoad) _data.options.afterLoad.call(_this)

            _this.updateSize()
          }, // end of AJAX callback
        }) // end of AJAX

        return _this
      }, // end of init
    } // end of methods

    var _this = this.methods
  } // end of mapSVG class

  /** $.FN **/
  $.fn.mapSvg2 = function (opts) {
    var id = $(this).attr("id")

    if (typeof opts == "object" && instances[id] === undefined) {
      instances[id] = new mapSVG(this, opts)
      return instances[id].methods.init(opts, this)
    } else if (instances[id]) {
      return instances[id].methods
    } else {
      return instances
    }
  } // end of $.fn.mapSvg
})(jQuery)

// Tiny color
// TinyColor v1.3.0
// https://github.com/bgrins/TinyColor
// Brian Grinstead, MIT License
;(function (Math) {
  var trimLeft = /^\s+/,
    trimRight = /\s+$/,
    tinyCounter = 0,
    mathRound = Math.round,
    mathMin = Math.min,
    mathMax = Math.max,
    mathRandom = Math.random

  function tinycolor(color, opts) {
    color = color ? color : ""
    opts = opts || {}

    // If input is already a tinycolor, return itself
    if (color instanceof tinycolor) {
      return color
    }
    // If we are called as a function, call using new instead
    if (!(this instanceof tinycolor)) {
      return new tinycolor(color, opts)
    }

    var rgb = inputToRGB(color)
    ;(this._originalInput = color),
      (this._r = rgb.r),
      (this._g = rgb.g),
      (this._b = rgb.b),
      (this._a = rgb.a),
      (this._roundA = mathRound(100 * this._a) / 100),
      (this._format = opts.format || rgb.format)
    this._gradientType = opts.gradientType

    // Don't let the range of [0,255] come back in [0,1].
    // Potentially lose a little bit of precision here, but will fix issues where
    // .5 gets interpreted as half of the total, instead of half of 1
    // If it was supposed to be 128, this was already taken care of by `inputToRgb`
    if (this._r < 1) {
      this._r = mathRound(this._r)
    }
    if (this._g < 1) {
      this._g = mathRound(this._g)
    }
    if (this._b < 1) {
      this._b = mathRound(this._b)
    }

    this._ok = rgb.ok
    this._tc_id = tinyCounter++
  }

  tinycolor.prototype = {
    isDark: function () {
      return this.getBrightness() < 128
    },
    isLight: function () {
      return !this.isDark()
    },
    isValid: function () {
      return this._ok
    },
    getOriginalInput: function () {
      return this._originalInput
    },
    getFormat: function () {
      return this._format
    },
    getAlpha: function () {
      return this._a
    },
    getBrightness: function () {
      //http://www.w3.org/TR/AERT#color-contrast
      var rgb = this.toRgb()
      return (rgb.r * 299 + rgb.g * 587 + rgb.b * 114) / 1000
    },
    getLuminance: function () {
      //http://www.w3.org/TR/2008/REC-WCAG20-20081211/#relativeluminancedef
      var rgb = this.toRgb()
      var RsRGB, GsRGB, BsRGB, R, G, B
      RsRGB = rgb.r / 255
      GsRGB = rgb.g / 255
      BsRGB = rgb.b / 255

      if (RsRGB <= 0.03928) {
        R = RsRGB / 12.92
      } else {
        R = Math.pow((RsRGB + 0.055) / 1.055, 2.4)
      }
      if (GsRGB <= 0.03928) {
        G = GsRGB / 12.92
      } else {
        G = Math.pow((GsRGB + 0.055) / 1.055, 2.4)
      }
      if (BsRGB <= 0.03928) {
        B = BsRGB / 12.92
      } else {
        B = Math.pow((BsRGB + 0.055) / 1.055, 2.4)
      }
      return 0.2126 * R + 0.7152 * G + 0.0722 * B
    },
    setAlpha: function (value) {
      this._a = boundAlpha(value)
      this._roundA = mathRound(100 * this._a) / 100
      return this
    },
    toHsv: function () {
      var hsv = rgbToHsv(this._r, this._g, this._b)
      return { h: hsv.h * 360, s: hsv.s, v: hsv.v, a: this._a }
    },
    toHsvString: function () {
      var hsv = rgbToHsv(this._r, this._g, this._b)
      var h = mathRound(hsv.h * 360),
        s = mathRound(hsv.s * 100),
        v = mathRound(hsv.v * 100)
      return this._a == 1
        ? "hsv(" + h + ", " + s + "%, " + v + "%)"
        : "hsva(" + h + ", " + s + "%, " + v + "%, " + this._roundA + ")"
    },
    toHsl: function () {
      var hsl = rgbToHsl(this._r, this._g, this._b)
      return { h: hsl.h * 360, s: hsl.s, l: hsl.l, a: this._a }
    },
    toHslString: function () {
      var hsl = rgbToHsl(this._r, this._g, this._b)
      var h = mathRound(hsl.h * 360),
        s = mathRound(hsl.s * 100),
        l = mathRound(hsl.l * 100)
      return this._a == 1
        ? "hsl(" + h + ", " + s + "%, " + l + "%)"
        : "hsla(" + h + ", " + s + "%, " + l + "%, " + this._roundA + ")"
    },
    toHex: function (allow3Char) {
      return rgbToHex(this._r, this._g, this._b, allow3Char)
    },
    toHexString: function (allow3Char) {
      return "#" + this.toHex(allow3Char)
    },
    toHex8: function () {
      return rgbaToHex(this._r, this._g, this._b, this._a)
    },
    toHex8String: function () {
      return "#" + this.toHex8()
    },
    toRgb: function () {
      return {
        r: mathRound(this._r),
        g: mathRound(this._g),
        b: mathRound(this._b),
        a: this._a,
      }
    },
    toRgbString: function () {
      return this._a == 1
        ? "rgb(" + mathRound(this._r) + ", " + mathRound(this._g) + ", " + mathRound(this._b) + ")"
        : "rgba(" +
            mathRound(this._r) +
            ", " +
            mathRound(this._g) +
            ", " +
            mathRound(this._b) +
            ", " +
            this._roundA +
            ")"
    },
    toPercentageRgb: function () {
      return {
        r: mathRound(bound01(this._r, 255) * 100) + "%",
        g: mathRound(bound01(this._g, 255) * 100) + "%",
        b: mathRound(bound01(this._b, 255) * 100) + "%",
        a: this._a,
      }
    },
    toPercentageRgbString: function () {
      return this._a == 1
        ? "rgb(" +
            mathRound(bound01(this._r, 255) * 100) +
            "%, " +
            mathRound(bound01(this._g, 255) * 100) +
            "%, " +
            mathRound(bound01(this._b, 255) * 100) +
            "%)"
        : "rgba(" +
            mathRound(bound01(this._r, 255) * 100) +
            "%, " +
            mathRound(bound01(this._g, 255) * 100) +
            "%, " +
            mathRound(bound01(this._b, 255) * 100) +
            "%, " +
            this._roundA +
            ")"
    },
    toName: function () {
      if (this._a === 0) {
        return "transparent"
      }

      if (this._a < 1) {
        return false
      }

      return hexNames[rgbToHex(this._r, this._g, this._b, true)] || false
    },
    toFilter: function (secondColor) {
      var hex8String = "#" + rgbaToHex(this._r, this._g, this._b, this._a)
      var secondHex8String = hex8String
      var gradientType = this._gradientType ? "GradientType = 1, " : ""

      if (secondColor) {
        var s = tinycolor(secondColor)
        secondHex8String = s.toHex8String()
      }

      return (
        "progid:DXImageTransform.Microsoft.gradient(" +
        gradientType +
        "startColorstr=" +
        hex8String +
        ",endColorstr=" +
        secondHex8String +
        ")"
      )
    },
    toString: function (format) {
      var formatSet = !!format
      format = format || this._format

      var formattedString = false
      var hasAlpha = this._a < 1 && this._a >= 0
      var needsAlphaFormat =
        !formatSet &&
        hasAlpha &&
        (format === "hex" || format === "hex6" || format === "hex3" || format === "name")

      if (needsAlphaFormat) {
        // Special case for "transparent", all other non-alpha formats
        // will return rgba when there is transparency.
        if (format === "name" && this._a === 0) {
          return this.toName()
        }
        return this.toRgbString()
      }
      if (format === "rgb") {
        formattedString = this.toRgbString()
      }
      if (format === "prgb") {
        formattedString = this.toPercentageRgbString()
      }
      if (format === "hex" || format === "hex6") {
        formattedString = this.toHexString()
      }
      if (format === "hex3") {
        formattedString = this.toHexString(true)
      }
      if (format === "hex8") {
        formattedString = this.toHex8String()
      }
      if (format === "name") {
        formattedString = this.toName()
      }
      if (format === "hsl") {
        formattedString = this.toHslString()
      }
      if (format === "hsv") {
        formattedString = this.toHsvString()
      }

      return formattedString || this.toHexString()
    },
    clone: function () {
      return tinycolor(this.toString())
    },

    _applyModification: function (fn, args) {
      var color = fn.apply(null, [this].concat([].slice.call(args)))
      this._r = color._r
      this._g = color._g
      this._b = color._b
      this.setAlpha(color._a)
      return this
    },
    lighten: function () {
      return this._applyModification(lighten, arguments)
    },
    brighten: function () {
      return this._applyModification(brighten, arguments)
    },
    darken: function () {
      return this._applyModification(darken, arguments)
    },
    desaturate: function () {
      return this._applyModification(desaturate, arguments)
    },
    saturate: function () {
      return this._applyModification(saturate, arguments)
    },
    greyscale: function () {
      return this._applyModification(greyscale, arguments)
    },
    spin: function () {
      return this._applyModification(spin, arguments)
    },

    _applyCombination: function (fn, args) {
      return fn.apply(null, [this].concat([].slice.call(args)))
    },
    analogous: function () {
      return this._applyCombination(analogous, arguments)
    },
    complement: function () {
      return this._applyCombination(complement, arguments)
    },
    monochromatic: function () {
      return this._applyCombination(monochromatic, arguments)
    },
    splitcomplement: function () {
      return this._applyCombination(splitcomplement, arguments)
    },
    triad: function () {
      return this._applyCombination(triad, arguments)
    },
    tetrad: function () {
      return this._applyCombination(tetrad, arguments)
    },
  }

  // If input is an object, force 1 into "1.0" to handle ratios properly
  // String input requires "1.0" as input, so 1 will be treated as 1
  tinycolor.fromRatio = function (color, opts) {
    if (typeof color == "object") {
      var newColor = {}
      for (var i in color) {
        if (color.hasOwnProperty(i)) {
          if (i === "a") {
            newColor[i] = color[i]
          } else {
            newColor[i] = convertToPercentage(color[i])
          }
        }
      }
      color = newColor
    }

    return tinycolor(color, opts)
  }

  // Given a string or object, convert that input to RGB
  // Possible string inputs:
  //
  //     "red"
  //     "#f00" or "f00"
  //     "#ff0000" or "ff0000"
  //     "#ff000000" or "ff000000"
  //     "rgb 255 0 0" or "rgb (255, 0, 0)"
  //     "rgb 1.0 0 0" or "rgb (1, 0, 0)"
  //     "rgba (255, 0, 0, 1)" or "rgba 255, 0, 0, 1"
  //     "rgba (1.0, 0, 0, 1)" or "rgba 1.0, 0, 0, 1"
  //     "hsl(0, 100%, 50%)" or "hsl 0 100% 50%"
  //     "hsla(0, 100%, 50%, 1)" or "hsla 0 100% 50%, 1"
  //     "hsv(0, 100%, 100%)" or "hsv 0 100% 100%"
  //
  function inputToRGB(color) {
    var rgb = { r: 0, g: 0, b: 0 }
    var a = 1
    var ok = false
    var format = false

    if (typeof color == "string") {
      color = stringInputToObject(color)
    }

    if (typeof color == "object") {
      if (isValidCSSUnit(color.r) && isValidCSSUnit(color.g) && isValidCSSUnit(color.b)) {
        rgb = rgbToRgb(color.r, color.g, color.b)
        ok = true
        format = String(color.r).substr(-1) === "%" ? "prgb" : "rgb"
      } else if (isValidCSSUnit(color.h) && isValidCSSUnit(color.s) && isValidCSSUnit(color.v)) {
        color.s = convertToPercentage(color.s)
        color.v = convertToPercentage(color.v)
        rgb = hsvToRgb(color.h, color.s, color.v)
        ok = true
        format = "hsv"
      } else if (isValidCSSUnit(color.h) && isValidCSSUnit(color.s) && isValidCSSUnit(color.l)) {
        color.s = convertToPercentage(color.s)
        color.l = convertToPercentage(color.l)
        rgb = hslToRgb(color.h, color.s, color.l)
        ok = true
        format = "hsl"
      }

      if (color.hasOwnProperty("a")) {
        a = color.a
      }
    }

    a = boundAlpha(a)

    return {
      ok: ok,
      format: color.format || format,
      r: mathMin(255, mathMax(rgb.r, 0)),
      g: mathMin(255, mathMax(rgb.g, 0)),
      b: mathMin(255, mathMax(rgb.b, 0)),
      a: a,
    }
  }

  // Conversion Functions
  // --------------------

  // `rgbToHsl`, `rgbToHsv`, `hslToRgb`, `hsvToRgb` modified from:
  // <http://mjijackson.com/2008/02/rgb-to-hsl-and-rgb-to-hsv-color-model-conversion-algorithms-in-javascript>

  // `rgbToRgb`
  // Handle bounds / percentage checking to conform to CSS color spec
  // <http://www.w3.org/TR/css3-color/>
  // *Assumes:* r, g, b in [0, 255] or [0, 1]
  // *Returns:* { r, g, b } in [0, 255]
  function rgbToRgb(r, g, b) {
    return {
      r: bound01(r, 255) * 255,
      g: bound01(g, 255) * 255,
      b: bound01(b, 255) * 255,
    }
  }

  // `rgbToHsl`
  // Converts an RGB color value to HSL.
  // *Assumes:* r, g, and b are contained in [0, 255] or [0, 1]
  // *Returns:* { h, s, l } in [0,1]
  function rgbToHsl(r, g, b) {
    r = bound01(r, 255)
    g = bound01(g, 255)
    b = bound01(b, 255)

    var max = mathMax(r, g, b),
      min = mathMin(r, g, b)
    var h,
      s,
      l = (max + min) / 2

    if (max == min) {
      h = s = 0 // achromatic
    } else {
      var d = max - min
      s = l > 0.5 ? d / (2 - max - min) : d / (max + min)
      switch (max) {
        case r:
          h = (g - b) / d + (g < b ? 6 : 0)
          break
        case g:
          h = (b - r) / d + 2
          break
        case b:
          h = (r - g) / d + 4
          break
      }

      h /= 6
    }

    return { h: h, s: s, l: l }
  }

  // `hslToRgb`
  // Converts an HSL color value to RGB.
  // *Assumes:* h is contained in [0, 1] or [0, 360] and s and l are contained [0, 1] or [0, 100]
  // *Returns:* { r, g, b } in the set [0, 255]
  function hslToRgb(h, s, l) {
    var r, g, b

    h = bound01(h, 360)
    s = bound01(s, 100)
    l = bound01(l, 100)

    function hue2rgb(p, q, t) {
      if (t < 0) t += 1
      if (t > 1) t -= 1
      if (t < 1 / 6) return p + (q - p) * 6 * t
      if (t < 1 / 2) return q
      if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6
      return p
    }

    if (s === 0) {
      r = g = b = l // achromatic
    } else {
      var q = l < 0.5 ? l * (1 + s) : l + s - l * s
      var p = 2 * l - q
      r = hue2rgb(p, q, h + 1 / 3)
      g = hue2rgb(p, q, h)
      b = hue2rgb(p, q, h - 1 / 3)
    }

    return { r: r * 255, g: g * 255, b: b * 255 }
  }

  // `rgbToHsv`
  // Converts an RGB color value to HSV
  // *Assumes:* r, g, and b are contained in the set [0, 255] or [0, 1]
  // *Returns:* { h, s, v } in [0,1]
  function rgbToHsv(r, g, b) {
    r = bound01(r, 255)
    g = bound01(g, 255)
    b = bound01(b, 255)

    var max = mathMax(r, g, b),
      min = mathMin(r, g, b)
    var h,
      s,
      v = max

    var d = max - min
    s = max === 0 ? 0 : d / max

    if (max == min) {
      h = 0 // achromatic
    } else {
      switch (max) {
        case r:
          h = (g - b) / d + (g < b ? 6 : 0)
          break
        case g:
          h = (b - r) / d + 2
          break
        case b:
          h = (r - g) / d + 4
          break
      }
      h /= 6
    }
    return { h: h, s: s, v: v }
  }

  // `hsvToRgb`
  // Converts an HSV color value to RGB.
  // *Assumes:* h is contained in [0, 1] or [0, 360] and s and v are contained in [0, 1] or [0, 100]
  // *Returns:* { r, g, b } in the set [0, 255]
  function hsvToRgb(h, s, v) {
    h = bound01(h, 360) * 6
    s = bound01(s, 100)
    v = bound01(v, 100)

    var i = Math.floor(h),
      f = h - i,
      p = v * (1 - s),
      q = v * (1 - f * s),
      t = v * (1 - (1 - f) * s),
      mod = i % 6,
      r = [v, q, p, p, t, v][mod],
      g = [t, v, v, q, p, p][mod],
      b = [p, p, t, v, v, q][mod]

    return { r: r * 255, g: g * 255, b: b * 255 }
  }

  // `rgbToHex`
  // Converts an RGB color to hex
  // Assumes r, g, and b are contained in the set [0, 255]
  // Returns a 3 or 6 character hex
  function rgbToHex(r, g, b, allow3Char) {
    var hex = [
      pad2(mathRound(r).toString(16)),
      pad2(mathRound(g).toString(16)),
      pad2(mathRound(b).toString(16)),
    ]

    // Return a 3 character hex if possible
    if (
      allow3Char &&
      hex[0].charAt(0) == hex[0].charAt(1) &&
      hex[1].charAt(0) == hex[1].charAt(1) &&
      hex[2].charAt(0) == hex[2].charAt(1)
    ) {
      return hex[0].charAt(0) + hex[1].charAt(0) + hex[2].charAt(0)
    }

    return hex.join("")
  }

  // `rgbaToHex`
  // Converts an RGBA color plus alpha transparency to hex
  // Assumes r, g, b and a are contained in the set [0, 255]
  // Returns an 8 character hex
  function rgbaToHex(r, g, b, a) {
    var hex = [
      pad2(convertDecimalToHex(a)),
      pad2(mathRound(r).toString(16)),
      pad2(mathRound(g).toString(16)),
      pad2(mathRound(b).toString(16)),
    ]

    return hex.join("")
  }

  // `equals`
  // Can be called with any tinycolor input
  tinycolor.equals = function (color1, color2) {
    if (!color1 || !color2) {
      return false
    }
    return tinycolor(color1).toRgbString() == tinycolor(color2).toRgbString()
  }

  tinycolor.random = function () {
    return tinycolor.fromRatio({
      r: mathRandom(),
      g: mathRandom(),
      b: mathRandom(),
    })
  }

  // Modification Functions
  // ----------------------
  // Thanks to less.js for some of the basics here
  // <https://github.com/cloudhead/less.js/blob/master/lib/less/functions.js>

  function desaturate(color, amount) {
    amount = amount === 0 ? 0 : amount || 10
    var hsl = tinycolor(color).toHsl()
    hsl.s -= amount / 100
    hsl.s = clamp01(hsl.s)
    return tinycolor(hsl)
  }

  function saturate(color, amount) {
    amount = amount === 0 ? 0 : amount || 10
    var hsl = tinycolor(color).toHsl()
    hsl.s += amount / 100
    hsl.s = clamp01(hsl.s)
    return tinycolor(hsl)
  }

  function greyscale(color) {
    return tinycolor(color).desaturate(100)
  }

  function lighten(color, amount) {
    amount = amount === 0 ? 0 : amount || 10
    var hsl = tinycolor(color).toHsl()
    hsl.l += amount / 100
    hsl.l = clamp01(hsl.l)
    return tinycolor(hsl)
  }

  function brighten(color, amount) {
    amount = amount === 0 ? 0 : amount || 10
    var rgb = tinycolor(color).toRgb()
    rgb.r = mathMax(0, mathMin(255, rgb.r - mathRound(255 * -(amount / 100))))
    rgb.g = mathMax(0, mathMin(255, rgb.g - mathRound(255 * -(amount / 100))))
    rgb.b = mathMax(0, mathMin(255, rgb.b - mathRound(255 * -(amount / 100))))
    return tinycolor(rgb)
  }

  function darken(color, amount) {
    amount = amount === 0 ? 0 : amount || 10
    var hsl = tinycolor(color).toHsl()
    hsl.l -= amount / 100
    hsl.l = clamp01(hsl.l)
    return tinycolor(hsl)
  }

  // Spin takes a positive or negative amount within [-360, 360] indicating the change of hue.
  // Values outside of this range will be wrapped into this range.
  function spin(color, amount) {
    var hsl = tinycolor(color).toHsl()
    var hue = (hsl.h + amount) % 360
    hsl.h = hue < 0 ? 360 + hue : hue
    return tinycolor(hsl)
  }

  // Combination Functions
  // ---------------------
  // Thanks to jQuery xColor for some of the ideas behind these
  // <https://github.com/infusion/jQuery-xcolor/blob/master/jquery.xcolor.js>

  function complement(color) {
    var hsl = tinycolor(color).toHsl()
    hsl.h = (hsl.h + 180) % 360
    return tinycolor(hsl)
  }

  function triad(color) {
    var hsl = tinycolor(color).toHsl()
    var h = hsl.h
    return [
      tinycolor(color),
      tinycolor({ h: (h + 120) % 360, s: hsl.s, l: hsl.l }),
      tinycolor({ h: (h + 240) % 360, s: hsl.s, l: hsl.l }),
    ]
  }

  function tetrad(color) {
    var hsl = tinycolor(color).toHsl()
    var h = hsl.h
    return [
      tinycolor(color),
      tinycolor({ h: (h + 90) % 360, s: hsl.s, l: hsl.l }),
      tinycolor({ h: (h + 180) % 360, s: hsl.s, l: hsl.l }),
      tinycolor({ h: (h + 270) % 360, s: hsl.s, l: hsl.l }),
    ]
  }

  function splitcomplement(color) {
    var hsl = tinycolor(color).toHsl()
    var h = hsl.h
    return [
      tinycolor(color),
      tinycolor({ h: (h + 72) % 360, s: hsl.s, l: hsl.l }),
      tinycolor({ h: (h + 216) % 360, s: hsl.s, l: hsl.l }),
    ]
  }

  function analogous(color, results, slices) {
    results = results || 6
    slices = slices || 30

    var hsl = tinycolor(color).toHsl()
    var part = 360 / slices
    var ret = [tinycolor(color)]

    for (hsl.h = (hsl.h - ((part * results) >> 1) + 720) % 360; --results; ) {
      hsl.h = (hsl.h + part) % 360
      ret.push(tinycolor(hsl))
    }
    return ret
  }

  function monochromatic(color, results) {
    results = results || 6
    var hsv = tinycolor(color).toHsv()
    var h = hsv.h,
      s = hsv.s,
      v = hsv.v
    var ret = []
    var modification = 1 / results

    while (results--) {
      ret.push(tinycolor({ h: h, s: s, v: v }))
      v = (v + modification) % 1
    }

    return ret
  }

  // Utility Functions
  // ---------------------

  tinycolor.mix = function (color1, color2, amount) {
    amount = amount === 0 ? 0 : amount || 50

    var rgb1 = tinycolor(color1).toRgb()
    var rgb2 = tinycolor(color2).toRgb()

    var p = amount / 100
    var w = p * 2 - 1
    var a = rgb2.a - rgb1.a

    var w1

    if (w * a == -1) {
      w1 = w
    } else {
      w1 = (w + a) / (1 + w * a)
    }

    w1 = (w1 + 1) / 2

    var w2 = 1 - w1

    var rgba = {
      r: rgb2.r * w1 + rgb1.r * w2,
      g: rgb2.g * w1 + rgb1.g * w2,
      b: rgb2.b * w1 + rgb1.b * w2,
      a: rgb2.a * p + rgb1.a * (1 - p),
    }

    return tinycolor(rgba)
  }

  // Readability Functions
  // ---------------------
  // <http://www.w3.org/TR/2008/REC-WCAG20-20081211/#contrast-ratiodef (WCAG Version 2)

  // `contrast`
  // Analyze the 2 colors and returns the color contrast defined by (WCAG Version 2)
  tinycolor.readability = function (color1, color2) {
    var c1 = tinycolor(color1)
    var c2 = tinycolor(color2)
    return (
      (Math.max(c1.getLuminance(), c2.getLuminance()) + 0.05) /
      (Math.min(c1.getLuminance(), c2.getLuminance()) + 0.05)
    )
  }

  // `isReadable`
  // Ensure that foreground and background color combinations meet WCAG2 guidelines.
  // The third argument is an optional Object.
  //      the 'level' property states 'AA' or 'AAA' - if missing or invalid, it defaults to 'AA';
  //      the 'size' property states 'large' or 'small' - if missing or invalid, it defaults to 'small'.
  // If the entire object is absent, isReadable defaults to {level:"AA",size:"small"}.

  // *Example*
  //    tinycolor.isReadable("#000", "#111") => false
  //    tinycolor.isReadable("#000", "#111",{level:"AA",size:"large"}) => false
  tinycolor.isReadable = function (color1, color2, wcag2) {
    var readability = tinycolor.readability(color1, color2)
    var wcag2Parms, out

    out = false

    wcag2Parms = validateWCAG2Parms(wcag2)
    switch (wcag2Parms.level + wcag2Parms.size) {
      case "AAsmall":
      case "AAAlarge":
        out = readability >= 4.5
        break
      case "AAlarge":
        out = readability >= 3
        break
      case "AAAsmall":
        out = readability >= 7
        break
    }
    return out
  }

  // `mostReadable`
  // Given a base color and a list of possible foreground or background
  // colors for that base, returns the most readable color.
  // Optionally returns Black or White if the most readable color is unreadable.
  // *Example*
  //    tinycolor.mostReadable(tinycolor.mostReadable("#123", ["#124", "#125"],{includeFallbackColors:false}).toHexString(); // "#112255"
  //    tinycolor.mostReadable(tinycolor.mostReadable("#123", ["#124", "#125"],{includeFallbackColors:true}).toHexString();  // "#ffffff"
  //    tinycolor.mostReadable("#a8015a", ["#faf3f3"],{includeFallbackColors:true,level:"AAA",size:"large"}).toHexString(); // "#faf3f3"
  //    tinycolor.mostReadable("#a8015a", ["#faf3f3"],{includeFallbackColors:true,level:"AAA",size:"small"}).toHexString(); // "#ffffff"
  tinycolor.mostReadable = function (baseColor, colorList, args) {
    var bestColor = null
    var bestScore = 0
    var readability
    var includeFallbackColors, level, size
    args = args || {}
    includeFallbackColors = args.includeFallbackColors
    level = args.level
    size = args.size

    for (var i = 0; i < colorList.length; i++) {
      readability = tinycolor.readability(baseColor, colorList[i])
      if (readability > bestScore) {
        bestScore = readability
        bestColor = tinycolor(colorList[i])
      }
    }

    if (
      tinycolor.isReadable(baseColor, bestColor, { level: level, size: size }) ||
      !includeFallbackColors
    ) {
      return bestColor
    } else {
      args.includeFallbackColors = false
      return tinycolor.mostReadable(baseColor, ["#fff", "#000"], args)
    }
  }

  // Big List of Colors
  // ------------------
  // <http://www.w3.org/TR/css3-color/#svg-color>
  var names = (tinycolor.names = {
    aliceblue: "f0f8ff",
    antiquewhite: "faebd7",
    aqua: "0ff",
    aquamarine: "7fffd4",
    azure: "f0ffff",
    beige: "f5f5dc",
    bisque: "ffe4c4",
    black: "000",
    blanchedalmond: "ffebcd",
    blue: "00f",
    blueviolet: "8a2be2",
    brown: "a52a2a",
    burlywood: "deb887",
    burntsienna: "ea7e5d",
    cadetblue: "5f9ea0",
    chartreuse: "7fff00",
    chocolate: "d2691e",
    coral: "ff7f50",
    cornflowerblue: "6495ed",
    cornsilk: "fff8dc",
    crimson: "dc143c",
    cyan: "0ff",
    darkblue: "00008b",
    darkcyan: "008b8b",
    darkgoldenrod: "b8860b",
    darkgray: "a9a9a9",
    darkgreen: "006400",
    darkgrey: "a9a9a9",
    darkkhaki: "bdb76b",
    darkmagenta: "8b008b",
    darkolivegreen: "556b2f",
    darkorange: "ff8c00",
    darkorchid: "9932cc",
    darkred: "8b0000",
    darksalmon: "e9967a",
    darkseagreen: "8fbc8f",
    darkslateblue: "483d8b",
    darkslategray: "2f4f4f",
    darkslategrey: "2f4f4f",
    darkturquoise: "00ced1",
    darkviolet: "9400d3",
    deeppink: "ff1493",
    deepskyblue: "00bfff",
    dimgray: "696969",
    dimgrey: "696969",
    dodgerblue: "1e90ff",
    firebrick: "b22222",
    floralwhite: "fffaf0",
    forestgreen: "228b22",
    fuchsia: "f0f",
    gainsboro: "dcdcdc",
    ghostwhite: "f8f8ff",
    gold: "ffd700",
    goldenrod: "daa520",
    gray: "808080",
    green: "008000",
    greenyellow: "adff2f",
    grey: "808080",
    honeydew: "f0fff0",
    hotpink: "ff69b4",
    indianred: "cd5c5c",
    indigo: "4b0082",
    ivory: "fffff0",
    khaki: "f0e68c",
    lavender: "e6e6fa",
    lavenderblush: "fff0f5",
    lawngreen: "7cfc00",
    lemonchiffon: "fffacd",
    lightblue: "add8e6",
    lightcoral: "f08080",
    lightcyan: "e0ffff",
    lightgoldenrodyellow: "fafad2",
    lightgray: "d3d3d3",
    lightgreen: "90ee90",
    lightgrey: "d3d3d3",
    lightpink: "ffb6c1",
    lightsalmon: "ffa07a",
    lightseagreen: "20b2aa",
    lightskyblue: "87cefa",
    lightslategray: "789",
    lightslategrey: "789",
    lightsteelblue: "b0c4de",
    lightyellow: "ffffe0",
    lime: "0f0",
    limegreen: "32cd32",
    linen: "faf0e6",
    magenta: "f0f",
    maroon: "800000",
    mediumaquamarine: "66cdaa",
    mediumblue: "0000cd",
    mediumorchid: "ba55d3",
    mediumpurple: "9370db",
    mediumseagreen: "3cb371",
    mediumslateblue: "7b68ee",
    mediumspringgreen: "00fa9a",
    mediumturquoise: "48d1cc",
    mediumvioletred: "c71585",
    midnightblue: "191970",
    mintcream: "f5fffa",
    mistyrose: "ffe4e1",
    moccasin: "ffe4b5",
    navajowhite: "ffdead",
    navy: "000080",
    oldlace: "fdf5e6",
    olive: "808000",
    olivedrab: "6b8e23",
    orange: "ffa500",
    orangered: "ff4500",
    orchid: "da70d6",
    palegoldenrod: "eee8aa",
    palegreen: "98fb98",
    paleturquoise: "afeeee",
    palevioletred: "db7093",
    papayawhip: "ffefd5",
    peachpuff: "ffdab9",
    peru: "cd853f",
    pink: "ffc0cb",
    plum: "dda0dd",
    powderblue: "b0e0e6",
    purple: "800080",
    rebeccapurple: "663399",
    red: "f00",
    rosybrown: "bc8f8f",
    royalblue: "4169e1",
    saddlebrown: "8b4513",
    salmon: "fa8072",
    sandybrown: "f4a460",
    seagreen: "2e8b57",
    seashell: "fff5ee",
    sienna: "a0522d",
    silver: "c0c0c0",
    skyblue: "87ceeb",
    slateblue: "6a5acd",
    slategray: "708090",
    slategrey: "708090",
    snow: "fffafa",
    springgreen: "00ff7f",
    steelblue: "4682b4",
    tan: "d2b48c",
    teal: "008080",
    thistle: "d8bfd8",
    tomato: "ff6347",
    turquoise: "40e0d0",
    violet: "ee82ee",
    wheat: "f5deb3",
    white: "fff",
    whitesmoke: "f5f5f5",
    yellow: "ff0",
    yellowgreen: "9acd32",
  })

  // Make it easy to access colors via `hexNames[hex]`
  var hexNames = (tinycolor.hexNames = flip(names))

  // Utilities
  // ---------

  // `{ 'name1': 'val1' }` becomes `{ 'val1': 'name1' }`
  function flip(o) {
    var flipped = {}
    for (var i in o) {
      if (o.hasOwnProperty(i)) {
        flipped[o[i]] = i
      }
    }
    return flipped
  }

  // Return a valid alpha value [0,1] with all invalid values being set to 1
  function boundAlpha(a) {
    a = parseFloat(a)

    if (isNaN(a) || a < 0 || a > 1) {
      a = 1
    }

    return a
  }

  // Take input from [0, n] and return it as [0, 1]
  function bound01(n, max) {
    if (isOnePointZero(n)) {
      n = "100%"
    }

    var processPercent = isPercentage(n)
    n = mathMin(max, mathMax(0, parseFloat(n)))

    // Automatically convert percentage into number
    if (processPercent) {
      n = parseInt(n * max, 10) / 100
    }

    // Handle floating point rounding errors
    if (Math.abs(n - max) < 0.000001) {
      return 1
    }

    // Convert into [0, 1] range if it isn't already
    return (n % max) / parseFloat(max)
  }

  // Force a number between 0 and 1
  function clamp01(val) {
    return mathMin(1, mathMax(0, val))
  }

  // Parse a base-16 hex value into a base-10 integer
  function parseIntFromHex(val) {
    return parseInt(val, 16)
  }

  // Need to handle 1.0 as 100%, since once it is a number, there is no difference between it and 1
  // <http://stackoverflow.com/questions/7422072/javascript-how-to-detect-number-as-a-decimal-including-1-0>
  function isOnePointZero(n) {
    return typeof n == "string" && n.indexOf(".") != -1 && parseFloat(n) === 1
  }

  // Check to see if string passed in is a percentage
  function isPercentage(n) {
    return typeof n === "string" && n.indexOf("%") != -1
  }

  // Force a hex value to have 2 characters
  function pad2(c) {
    return c.length == 1 ? "0" + c : "" + c
  }

  // Replace a decimal with it's percentage value
  function convertToPercentage(n) {
    if (n <= 1) {
      n = n * 100 + "%"
    }

    return n
  }

  // Converts a decimal to a hex value
  function convertDecimalToHex(d) {
    return Math.round(parseFloat(d) * 255).toString(16)
  }
  // Converts a hex value to a decimal
  function convertHexToDecimal(h) {
    return parseIntFromHex(h) / 255
  }

  var matchers = (function () {
    // <http://www.w3.org/TR/css3-values/#integers>
    var CSS_INTEGER = "[-\\+]?\\d+%?"

    // <http://www.w3.org/TR/css3-values/#number-value>
    var CSS_NUMBER = "[-\\+]?\\d*\\.\\d+%?"

    // Allow positive/negative integer/number.  Don't capture the either/or, just the entire outcome.
    var CSS_UNIT = "(?:" + CSS_NUMBER + ")|(?:" + CSS_INTEGER + ")"

    // Actual matching.
    // Parentheses and commas are optional, but not required.
    // Whitespace can take the place of commas or opening paren
    var PERMISSIVE_MATCH3 =
      "[\\s|\\(]+(" + CSS_UNIT + ")[,|\\s]+(" + CSS_UNIT + ")[,|\\s]+(" + CSS_UNIT + ")\\s*\\)?"
    var PERMISSIVE_MATCH4 =
      "[\\s|\\(]+(" +
      CSS_UNIT +
      ")[,|\\s]+(" +
      CSS_UNIT +
      ")[,|\\s]+(" +
      CSS_UNIT +
      ")[,|\\s]+(" +
      CSS_UNIT +
      ")\\s*\\)?"

    return {
      CSS_UNIT: new RegExp(CSS_UNIT),
      rgb: new RegExp("rgb" + PERMISSIVE_MATCH3),
      rgba: new RegExp("rgba" + PERMISSIVE_MATCH4),
      hsl: new RegExp("hsl" + PERMISSIVE_MATCH3),
      hsla: new RegExp("hsla" + PERMISSIVE_MATCH4),
      hsv: new RegExp("hsv" + PERMISSIVE_MATCH3),
      hsva: new RegExp("hsva" + PERMISSIVE_MATCH4),
      hex3: /^#?([0-9a-fA-F]{1})([0-9a-fA-F]{1})([0-9a-fA-F]{1})$/,
      hex6: /^#?([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})$/,
      hex8: /^#?([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})$/,
    }
  })()

  // `isValidCSSUnit`
  // Take in a single string / number and check to see if it looks like a CSS unit
  // (see `matchers` above for definition).
  function isValidCSSUnit(color) {
    return !!matchers.CSS_UNIT.exec(color)
  }

  // `stringInputToObject`
  // Permissive string parsing.  Take in a number of formats, and output an object
  // based on detected format.  Returns `{ r, g, b }` or `{ h, s, l }` or `{ h, s, v}`
  function stringInputToObject(color) {
    color = color.replace(trimLeft, "").replace(trimRight, "").toLowerCase()
    var named = false
    if (names[color]) {
      color = names[color]
      named = true
    } else if (color == "transparent") {
      return { r: 0, g: 0, b: 0, a: 0, format: "name" }
    }

    // Try to match string input using regular expressions.
    // Keep most of the number bounding out of this function - don't worry about [0,1] or [0,100] or [0,360]
    // Just return an object and let the conversion functions handle that.
    // This way the result will be the same whether the tinycolor is initialized with string or object.
    var match
    if ((match = matchers.rgb.exec(color))) {
      return { r: match[1], g: match[2], b: match[3] }
    }
    if ((match = matchers.rgba.exec(color))) {
      return { r: match[1], g: match[2], b: match[3], a: match[4] }
    }
    if ((match = matchers.hsl.exec(color))) {
      return { h: match[1], s: match[2], l: match[3] }
    }
    if ((match = matchers.hsla.exec(color))) {
      return { h: match[1], s: match[2], l: match[3], a: match[4] }
    }
    if ((match = matchers.hsv.exec(color))) {
      return { h: match[1], s: match[2], v: match[3] }
    }
    if ((match = matchers.hsva.exec(color))) {
      return { h: match[1], s: match[2], v: match[3], a: match[4] }
    }
    if ((match = matchers.hex8.exec(color))) {
      return {
        a: convertHexToDecimal(match[1]),
        r: parseIntFromHex(match[2]),
        g: parseIntFromHex(match[3]),
        b: parseIntFromHex(match[4]),
        format: named ? "name" : "hex8",
      }
    }
    if ((match = matchers.hex6.exec(color))) {
      return {
        r: parseIntFromHex(match[1]),
        g: parseIntFromHex(match[2]),
        b: parseIntFromHex(match[3]),
        format: named ? "name" : "hex",
      }
    }
    if ((match = matchers.hex3.exec(color))) {
      return {
        r: parseIntFromHex(match[1] + "" + match[1]),
        g: parseIntFromHex(match[2] + "" + match[2]),
        b: parseIntFromHex(match[3] + "" + match[3]),
        format: named ? "name" : "hex",
      }
    }

    return false
  }

  function validateWCAG2Parms(parms) {
    // return valid WCAG2 parms for isReadable.
    // If input parms are invalid, return {"level":"AA", "size":"small"}
    var level, size
    parms = parms || { level: "AA", size: "small" }
    level = (parms.level || "AA").toUpperCase()
    size = (parms.size || "small").toLowerCase()
    if (level !== "AA" && level !== "AAA") {
      level = "AA"
    }
    if (size !== "small" && size !== "large") {
      size = "small"
    }
    return { level: level, size: size }
  }

  // Node: Export function
  if (typeof module !== "undefined" && module.exports) {
    module.exports = tinycolor
  }
  // AMD/requirejs: Define the module
  else if (typeof define === "function" && define.amd) {
    define(function () {
      return tinycolor
    })
  }
  // Browser: Expose to window
  else {
    window.tinycolor = tinycolor
  }
})(Math)
