零件:InteractiveMap.js

来自俄罗斯方块中文维基
// ==UserScript==
// @name InteractiveMap
// @namespace Violentmonkey Scripts
// @match http*://nms.huijiwiki.com/wiki/%E6%97%A0%E4%BA%BA%E6%B7%B1%E7%A9%BA%E4%B8%AD%E6%96%87%E7%BB%B4%E5%9F%BA:%E6%B2%99%E7%9B%92
// @grant none
// @run-at document-idle
// @noframes
// ==/UserScript==
//<nowiki>
(function(leaflet, mw, $) {
  'use strict';
  $(document).ready(function() {
    if ($(mapClass).length === 0) {
      return;
    }
    init();
  });
  console.log('Loading interactive map module.');
  // define class and attr names
  var mapClass = '.interactive-map';
  var mapSourceAttr = 'data-mapSource';
  var mapMarkerAttr = 'data-markers';
  var mapInitialZoom = 'data-initialZoom';
  var mapInitialLocX = 'data-initialLocX';
  var mapInitialLocY = 'data-initialLocY';
  // default icon with modified path
  var defaultIcon = leaflet.icon({
    iconUrl:
      'https://huiji-public.huijistatic.com/warframe/uploads/7/7a/Marker-icon.png',
    shadowUrl:
      'https://huiji-public.huijistatic.com/warframe/uploads/d/d4/Marker-shadow.png',
    iconSize:      [25, 41],
    shadowSize:    [41, 41],
    iconAnchor:    [12, 41],
    shadowAnchor:  [12, 41],
    popupAnchor:   [0, -41],
    tooltipAnchor: [12, -30]
  });
  // store icon info to reduce redundant request
  // {iconname:{url,width,height},...}
  var iconMap = {};
  // map working mode enum
  var MODE = {
    OVERLAY: 'overlay',
    TILE:    'tile'
  };
  Object.freeze(MODE);
  // initialization function
  function init() {
    $(mapClass).each(function() {
      if (!$(this).attr('id') || $(this).prop('tagName') !== 'DIV') {
        $(this).text('地图的id或标签设置有误!');
        return;
      }
      // {image, markerPage, imageUrl, markerData([{coords{x,y},tooltip,markerImage},...]), markersLayerGroup, imageWidth, imageHeight, initialZoom, initialLocX, initialLocY, mapId, needSave}
      var mapInfo = {};
      mapInfo.mapId = $(this).attr('id');
      mapInfo.image = $(this).attr(mapSourceAttr);
      mapInfo.markerPage = $(this).attr(mapMarkerAttr);
      mapInfo.initialZoom = Number($(this).attr(mapInitialZoom));
      mapInfo.initialLocX = Number($(this).attr(mapInitialLocX));
      mapInfo.initialLocY = Number($(this).attr(mapInitialLocY));
      mapInfo.tileTemplate = $(this).attr('data-tileTemplate');
      mapInfo.tileSize = $(this).attr('data-tileSize');
      mapInfo.tileBounds = $(this).attr('data-tileBounds');
      mapInfo.tileZoom = $(this).attr('data-tileZoom');
      if (!mapInfo.image) {
        if (!mapInfo.tileTemplate) {
          mapInfo.image = '';
          mapInfo.mode = MODE.OVERLAY;
        } else {
          mapInfo.tileTemplate = ucFirst(mapInfo.tileTemplate);
          mapInfo.mode = MODE.TILE;
        }
      } else {
        mapInfo.mode = MODE.OVERLAY;
      }
      if (!mapInfo.markerPage) {
        if (mapInfo.mode === MODE.OVERLAY) {
          if (mapInfo.image) {
            mapInfo.markerPage = 'Data:' + mapInfo.image + '.json';
          } else {
            mapInfo.markerPage = '';
          }
        } else {
          mapInfo.markerPage = 'Data:' + mapInfo.tileTemplate + '.json';
        }
      }
      // start async setup procedure
      setMapSrcUrl(mapInfo);
    });
    // help info modal
    $('body').append(
      '<div class="modal fade"id="map-help-info"tabindex="-1"role="dialog"aria-labelledby="myModalLabel"><div class="modal-dialog"role="document"><div class="modal-content"><div class="modal-header"><h3 class="modal-title">使用说明</h3></div><div class="modal-body"><h4 class="modal-title">添加标记</h4><p>在地图上点击鼠标右键,在弹出菜单中选择添加标记。</p><h4 class="modal-title">查看标记说明</h4><p>对于已经添加了说明信息的标记,可单击标记来查看说明文字。</p><h4 class="modal-title">编辑&nbsp;/&nbsp;删除标记</h4><p>在标记上点击鼠标右键,然后选择编辑&nbsp;/&nbsp;删除标记。</p><h4 class="modal-title">复制标记</h4><p>按住&nbsp;<span class="label label-default"style="padding: .2em .6em;">Ctrl</span>&nbsp;拖动一个已有标记,即可将该标记复制到目标位置。</p><h4 class="modal-title">保存改动</h4><p>对标记进行了修改后,点击地图右上角的&nbsp;<i class="fa fa-save"aria-hidden="true"></i>&nbsp;按钮来保存数据。</p><h4 class="modal-title">隐藏&nbsp;/&nbsp;显示标记</h4><p>点击地图左上角的&nbsp;<i class="fa fa-eye"aria-hidden="true"></i>&nbsp;按钮,来隐藏&nbsp;/&nbsp;显示标记。</p></div><div class="modal-footer"><button type="button"class="btn btn-default"data-dismiss="modal">关闭</button></div></div></div></div>'
    );
  }
  function requestWithCallback(
    queryData,
    mapInfo,
    callback,
    reqType,
    errCallback
  ) {
    if (!reqType) {
      reqType = 'GET';
    }
    if (!errCallback) {
      errCallback = function() {
        console.log('请求过程中出现错误!');
      };
    }
    $.ajax({
      url:      '/api.php',
      data:     queryData,
      type:     reqType,
      dataType: 'json',
      cache:    false,
      timeout:  10000,
      error:    function(xhr) {
        if (typeof errCallback === 'function') {
          errCallback(xhr, mapInfo);
        }
      },
      success: function(data) {
        if (typeof callback === 'function') {
          callback(data, mapInfo);
        }
      }
    });
  }
  function setMapSrcUrl(mapInfo) {
    if (mapInfo.mode === MODE.TILE) {
      setMarkersData(mapInfo);
      return;
    }
    var queryData = {
      action:        'query',
      format:        'json',
      prop:          'imageinfo',
      titles:        'File:' + mapInfo.image,
      formatversion: '2',
      iiprop:        'url|dimensions'
    };
    requestWithCallback(queryData, mapInfo, function(data, mapInfo) {
      var pages = data.query.pages;
      var pageid;
      for (var p in pages) {
        if (pages.hasOwnProperty(p)) {
          if (pages[p].hasOwnProperty('missing')) {
            $('#' + mapInfo.mapId).text('未找到地图:' + mapInfo.image);
            return;
          } else {
            pageid = p;
            break;
          }
        }
      }
      mapInfo.imageUrl = pages[pageid].imageinfo[0]['url'];
      mapInfo.imageWidth = pages[pageid].imageinfo[0]['width'];
      mapInfo.imageHeight = pages[pageid].imageinfo[0]['height'];
      setMarkersData(mapInfo);
    });
  }
  function setMarkersData(mapInfo, mapId) {
    var queryData = {
      action:        'query',
      format:        'json',
      prop:          'revisions',
      titles:        mapInfo.markerPage,
      formatversion: '2',
      rvprop:        'content',
      rvlimit:       '1'
    };
    requestWithCallback(queryData, mapInfo, function(data, mapInfo) {
      var pages = data.query.pages;
      var pageid;
      for (var p in pages) {
        if (pages.hasOwnProperty(p)) {
          if (!pages[p].hasOwnProperty('missing')) {
            try {
              mapInfo.markerData = JSON.parse(
                data.query.pages[0].revisions[0].content
              ).markers;
              if (
                typeof mapInfo.markerData !== 'object' ||
                typeof mapInfo.markerData === null
              ) {
                mapInfo.markerData = [];
              }
            } catch (error) {
              console.log(error.messge);
              mapInfo.markerData = [];
            }
            break;
          }
        }
      }
      if (!mapInfo.markerData) {
        mapInfo.markerData = [];
      }
      loadMap(mapInfo);
    });
  }

  function md5(string) {
    var AA,
      AddUnsigned,
      BB,
      CC,
      ConvertToWordArray,
      DD,
      F,
      FF,
      G,
      GG,
      H,
      HH,
      I,
      II,
      RotateLeft,
      S11,
      S12,
      S13,
      S14,
      S21,
      S22,
      S23,
      S24,
      S31,
      S32,
      S33,
      S34,
      S41,
      S42,
      S43,
      S44,
      Utf8Encode,
      WordToHex,
      a,
      b,
      c,
      d,
      k,
      temp,
      x;
    RotateLeft = function(lValue, iShiftBits) {
      return (lValue << iShiftBits) | (lValue >>> (32 - iShiftBits));
    };
    AddUnsigned = function(lX, lY) {
      var lResult, lX4, lX8, lY4, lY8;
      lX8 = lX & 0x80000000;
      lY8 = lY & 0x80000000;
      lX4 = lX & 0x40000000;
      lY4 = lY & 0x40000000;
      lResult = (lX & 0x3fffffff) + (lY & 0x3fffffff);
      if (lX4 & lY4) {
        return lResult ^ 0x80000000 ^ lX8 ^ lY8;
      }
      if (lX4 | lY4) {
        if (lResult & 0x40000000) {
          return lResult ^ 0xc0000000 ^ lX8 ^ lY8;
        } else {
          return lResult ^ 0x40000000 ^ lX8 ^ lY8;
        }
      } else {
        return lResult ^ lX8 ^ lY8;
      }
    };
    F = function(x, y, z) {
      return (x & y) | (~x & z);
    };
    G = function(x, y, z) {
      return (x & z) | (y & ~z);
    };
    H = function(x, y, z) {
      return x ^ y ^ z;
    };
    I = function(x, y, z) {
      return y ^ (x | ~z);
    };
    FF = function(a, b, c, d, x, s, ac) {
      a = AddUnsigned(a, AddUnsigned(AddUnsigned(F(b, c, d), x), ac));
      return AddUnsigned(RotateLeft(a, s), b);
    };
    GG = function(a, b, c, d, x, s, ac) {
      a = AddUnsigned(a, AddUnsigned(AddUnsigned(G(b, c, d), x), ac));
      return AddUnsigned(RotateLeft(a, s), b);
    };
    HH = function(a, b, c, d, x, s, ac) {
      a = AddUnsigned(a, AddUnsigned(AddUnsigned(H(b, c, d), x), ac));
      return AddUnsigned(RotateLeft(a, s), b);
    };
    II = function(a, b, c, d, x, s, ac) {
      a = AddUnsigned(a, AddUnsigned(AddUnsigned(I(b, c, d), x), ac));
      return AddUnsigned(RotateLeft(a, s), b);
    };
    ConvertToWordArray = function(string) {
      var lByteCount,
        lBytePosition,
        lMessageLength,
        lNumberOfWords,
        lNumberOfWords_temp1,
        lNumberOfWords_temp2,
        lWordArray,
        lWordCount;
      lMessageLength = string.length;
      lNumberOfWords_temp1 = lMessageLength + 8;
      lNumberOfWords_temp2 =
        (lNumberOfWords_temp1 - (lNumberOfWords_temp1 % 64)) / 64;
      lNumberOfWords = (lNumberOfWords_temp2 + 1) * 16;
      lWordArray = Array(lNumberOfWords - 1);
      lBytePosition = 0;
      lByteCount = 0;
      while (lByteCount < lMessageLength) {
        lWordCount = (lByteCount - (lByteCount % 4)) / 4;
        lBytePosition = (lByteCount % 4) * 8;
        lWordArray[lWordCount] =
          lWordArray[lWordCount] |
          (string.charCodeAt(lByteCount) << lBytePosition);
        lByteCount++;
      }
      lWordCount = (lByteCount - (lByteCount % 4)) / 4;
      lBytePosition = (lByteCount % 4) * 8;
      lWordArray[lWordCount] = lWordArray[lWordCount] | (0x80 << lBytePosition);
      lWordArray[lNumberOfWords - 2] = lMessageLength << 3;
      lWordArray[lNumberOfWords - 1] = lMessageLength >>> 29;
      return lWordArray;
    };
    WordToHex = function(lValue) {
      var WordToHexValue, WordToHexValue_temp, lByte, lCount;
      WordToHexValue = '';
      WordToHexValue_temp = '';
      lCount = 0;
      while (lCount <= 3) {
        lByte = (lValue >>> (lCount * 8)) & 255;
        WordToHexValue_temp = '0' + lByte.toString(16);
        WordToHexValue =
          WordToHexValue +
          WordToHexValue_temp.substr(WordToHexValue_temp.length - 2, 2);
        lCount++;
      }
      return WordToHexValue;
    };
    Utf8Encode = function(string) {
      var c, n, utftext;
      string = string.replace(/\r\n/g, '\n');
      utftext = '';
      n = 0;
      while (n < string.length) {
        c = string.charCodeAt(n);
        if (c < 128) {
          utftext += String.fromCharCode(c);
        } else if (c > 127 && c < 2048) {
          utftext += String.fromCharCode((c >> 6) | 192);
          utftext += String.fromCharCode((c & 63) | 128);
        } else {
          utftext += String.fromCharCode((c >> 12) | 224);
          utftext += String.fromCharCode(((c >> 6) & 63) | 128);
          utftext += String.fromCharCode((c & 63) | 128);
        }
        n++;
      }
      return utftext;
    };
    x = Array();
    S11 = 7;
    S12 = 12;
    S13 = 17;
    S14 = 22;
    S21 = 5;
    S22 = 9;
    S23 = 14;
    S24 = 20;
    S31 = 4;
    S32 = 11;
    S33 = 16;
    S34 = 23;
    S41 = 6;
    S42 = 10;
    S43 = 15;
    S44 = 21;
    string = Utf8Encode(string);
    x = ConvertToWordArray(string);
    a = 0x67452301;
    b = 0xefcdab89;
    c = 0x98badcfe;
    d = 0x10325476;
    k = 0;
    while (k < x.length) {
      AA = a;
      BB = b;
      CC = c;
      DD = d;
      a = FF(a, b, c, d, x[k + 0], S11, 0xd76aa478);
      d = FF(d, a, b, c, x[k + 1], S12, 0xe8c7b756);
      c = FF(c, d, a, b, x[k + 2], S13, 0x242070db);
      b = FF(b, c, d, a, x[k + 3], S14, 0xc1bdceee);
      a = FF(a, b, c, d, x[k + 4], S11, 0xf57c0faf);
      d = FF(d, a, b, c, x[k + 5], S12, 0x4787c62a);
      c = FF(c, d, a, b, x[k + 6], S13, 0xa8304613);
      b = FF(b, c, d, a, x[k + 7], S14, 0xfd469501);
      a = FF(a, b, c, d, x[k + 8], S11, 0x698098d8);
      d = FF(d, a, b, c, x[k + 9], S12, 0x8b44f7af);
      c = FF(c, d, a, b, x[k + 10], S13, 0xffff5bb1);
      b = FF(b, c, d, a, x[k + 11], S14, 0x895cd7be);
      a = FF(a, b, c, d, x[k + 12], S11, 0x6b901122);
      d = FF(d, a, b, c, x[k + 13], S12, 0xfd987193);
      c = FF(c, d, a, b, x[k + 14], S13, 0xa679438e);
      b = FF(b, c, d, a, x[k + 15], S14, 0x49b40821);
      a = GG(a, b, c, d, x[k + 1], S21, 0xf61e2562);
      d = GG(d, a, b, c, x[k + 6], S22, 0xc040b340);
      c = GG(c, d, a, b, x[k + 11], S23, 0x265e5a51);
      b = GG(b, c, d, a, x[k + 0], S24, 0xe9b6c7aa);
      a = GG(a, b, c, d, x[k + 5], S21, 0xd62f105d);
      d = GG(d, a, b, c, x[k + 10], S22, 0x2441453);
      c = GG(c, d, a, b, x[k + 15], S23, 0xd8a1e681);
      b = GG(b, c, d, a, x[k + 4], S24, 0xe7d3fbc8);
      a = GG(a, b, c, d, x[k + 9], S21, 0x21e1cde6);
      d = GG(d, a, b, c, x[k + 14], S22, 0xc33707d6);
      c = GG(c, d, a, b, x[k + 3], S23, 0xf4d50d87);
      b = GG(b, c, d, a, x[k + 8], S24, 0x455a14ed);
      a = GG(a, b, c, d, x[k + 13], S21, 0xa9e3e905);
      d = GG(d, a, b, c, x[k + 2], S22, 0xfcefa3f8);
      c = GG(c, d, a, b, x[k + 7], S23, 0x676f02d9);
      b = GG(b, c, d, a, x[k + 12], S24, 0x8d2a4c8a);
      a = HH(a, b, c, d, x[k + 5], S31, 0xfffa3942);
      d = HH(d, a, b, c, x[k + 8], S32, 0x8771f681);
      c = HH(c, d, a, b, x[k + 11], S33, 0x6d9d6122);
      b = HH(b, c, d, a, x[k + 14], S34, 0xfde5380c);
      a = HH(a, b, c, d, x[k + 1], S31, 0xa4beea44);
      d = HH(d, a, b, c, x[k + 4], S32, 0x4bdecfa9);
      c = HH(c, d, a, b, x[k + 7], S33, 0xf6bb4b60);
      b = HH(b, c, d, a, x[k + 10], S34, 0xbebfbc70);
      a = HH(a, b, c, d, x[k + 13], S31, 0x289b7ec6);
      d = HH(d, a, b, c, x[k + 0], S32, 0xeaa127fa);
      c = HH(c, d, a, b, x[k + 3], S33, 0xd4ef3085);
      b = HH(b, c, d, a, x[k + 6], S34, 0x4881d05);
      a = HH(a, b, c, d, x[k + 9], S31, 0xd9d4d039);
      d = HH(d, a, b, c, x[k + 12], S32, 0xe6db99e5);
      c = HH(c, d, a, b, x[k + 15], S33, 0x1fa27cf8);
      b = HH(b, c, d, a, x[k + 2], S34, 0xc4ac5665);
      a = II(a, b, c, d, x[k + 0], S41, 0xf4292244);
      d = II(d, a, b, c, x[k + 7], S42, 0x432aff97);
      c = II(c, d, a, b, x[k + 14], S43, 0xab9423a7);
      b = II(b, c, d, a, x[k + 5], S44, 0xfc93a039);
      a = II(a, b, c, d, x[k + 12], S41, 0x655b59c3);
      d = II(d, a, b, c, x[k + 3], S42, 0x8f0ccc92);
      c = II(c, d, a, b, x[k + 10], S43, 0xffeff47d);
      b = II(b, c, d, a, x[k + 1], S44, 0x85845dd1);
      a = II(a, b, c, d, x[k + 8], S41, 0x6fa87e4f);
      d = II(d, a, b, c, x[k + 15], S42, 0xfe2ce6e0);
      c = II(c, d, a, b, x[k + 6], S43, 0xa3014314);
      b = II(b, c, d, a, x[k + 13], S44, 0x4e0811a1);
      a = II(a, b, c, d, x[k + 4], S41, 0xf7537e82);
      d = II(d, a, b, c, x[k + 11], S42, 0xbd3af235);
      c = II(c, d, a, b, x[k + 2], S43, 0x2ad7d2bb);
      b = II(b, c, d, a, x[k + 9], S44, 0xeb86d391);
      a = AddUnsigned(a, AA);
      b = AddUnsigned(b, BB);
      c = AddUnsigned(c, CC);
      d = AddUnsigned(d, DD);
      k += 16;
    }
    temp = WordToHex(a) + WordToHex(b) + WordToHex(c) + WordToHex(d);
    return temp.toLowerCase();
  }

  function getImageUrl(filename) {
    var hex = md5(filename);
    return [
      'https://huiji-public.huijistatic.com/' +
        mw.config.get('wgHuijiPrefix') +
        '/uploads',
      hex[0],
      hex[0] + hex[1],
      filename
    ].join('/');
  }

  function parseNumPairParam(param) {
    var paramList = param.split(',');
    var result = [];
    $.each(paramList, function(index, value) {
      result.push(Number(value));
    });
    return result;
  }

  var NamePatternRegex = /\$x|\$y|\$z/g;
  leaflet.TileLayer.CustomLayer = leaflet.TileLayer.extend({
    getTileUrl: function(coords) {
      var x = coords.x,
        y = coords.y,
        z = this._getZoomForUrl();
      var fileName = this.options.namingPattern.replace(
        NamePatternRegex,
        function(m) {
          var subst = {
            $x: x,
            $y: y,
            $z: z
          };
          return subst[m];
        }
      );
      return getImageUrl(fileName);
    }
  });

  leaflet.tileLayer.customLayer = function(templateUrl, options) {
    return new leaflet.TileLayer.CustomLayer(templateUrl, options);
  };

  function loadMap(mapInfo) {
    if ($('#' + mapInfo.mapId).height() < 300) {
      $('#' + mapInfo.mapId).height(300);
    }
    var saveButton = leaflet.easyButton({
      position: 'topright',
      states:   [
        {
          stateName: 'need-save',
          icon:      'fa fa-save',
          title:     '发现改动,点此保存',
          onClick:   function(btn, map) {
            if (!mapInfo.needSave) {
              return;
            }
            // save marker data
            mapInfo.needSave = false;
            btn.state('saving-data');
            var newMarkerData = {};
            newMarkerData.markers = getMarkerData(mapInfo);
            var queryData = {
              format:        'json',
              action:        'edit',
              contentformat: 'application/json',
              title:         mapInfo.markerPage,
              summary:       '更新地图标记信息',
              text:          JSON.stringify(newMarkerData),
              minor:         true,
              token:         mw.user.tokens.get('csrfToken')
            };
            requestWithCallback(
              queryData,
              mapInfo,
              function(data, mapInfo) {
                btn.state('need-save');
                btn.disable();
              },
              'POST',
              function(data, mapInfo) {
                console.log('请求过程中出现错误!');
                mapInfo.needSave = true;
                btn.enable();
                btn.state('need-save');
              }
            );
          }
        },
        {
          stateName: 'saving-data',
          icon:      'fa fa-spin fa-cog',
          title:     '保存数据中,请稍候',
          onClick:   function(btn, map) {
            // do nothing
          }
        }
      ]
    });
    saveButton.disable();
    var toogleMarkerButton = leaflet.easyButton({
      position: 'topleft',
      states:   [
        {
          stateName: 'show',
          icon:      'fa fa-eye',
          title:     '点此隐藏标记',
          onClick:   function(btn, map) {
            map.removeLayer(mapInfo.markersLayerGroup);
            btn.state('hide');
          }
        },
        {
          stateName: 'hide',
          icon:      'fa fa-eye-slash',
          title:     '点此显示标记',
          onClick:   function(btn, map) {
            map.addLayer(mapInfo.markersLayerGroup);
            btn.state('show');
          }
        }
      ]
    });
    var helpButton = leaflet.easyButton({
      position: 'topright',
      states:   [
        {
          stateName: 'always',
          icon:      'fa fa-question',
          title:     '查看地图使用说明',
          onClick:   function(btn, map) {
            $('#map-help-info').modal('show');
          }
        }
      ]
    });
    var layer, bounds, minZoom, options;
    if (mapInfo.mode === MODE.OVERLAY) {
      bounds = [
        [0, 0],
        [mapInfo.imageHeight, mapInfo.imageWidth]
      ];
      minZoom = -5;
      options = {
        attribution: '查看<a href="/wiki/File:' + mapInfo.image + '">原图</a>'
      };
      layer = leaflet.imageOverlay(mapInfo.imageUrl, bounds, options);
      if (!mapInfo.initialZoom) {
        mapInfo.initialZoom = 0;
      }
      if (!mapInfo.initialLocY) {
        mapInfo.initialLocY = mapInfo.imageHeight / 2;
      }
      if (!mapInfo.initialLocX) {
        mapInfo.initialLocX = mapInfo.imageWidth / 2;
      }
    } else {
      var tileBounds = parseNumPairParam(mapInfo.tileBounds);
      var tileZoom = parseNumPairParam(mapInfo.tileZoom);
      var tileSize = parseNumPairParam(mapInfo.tileSize);
      var scale = Math.pow(2, tileZoom[1]);
      bounds = [
        [0 - tileBounds[1] / scale, 0],
        [0, tileBounds[0] / scale]
      ];
      minZoom = tileZoom[0];
      options = {
        tileSize:      leaflet.point(tileSize[0], tileSize[1]),
        minZoom:       minZoom,
        maxZoom:       tileZoom[1],
        bounds:        bounds,
        namingPattern: mapInfo.tileTemplate,
        attribution:   ''
      };
      layer = leaflet.tileLayer.customLayer('', options);
      if (!mapInfo.initialZoom) {
        mapInfo.initialZoom = minZoom;
      }
      if (!mapInfo.initialLocY) {
        mapInfo.initialLocY = bounds[0][0] / 2;
      }
      if (!mapInfo.initialLocX) {
        mapInfo.initialLocX = bounds[1][1] / 2;
      }
    }
    var map = leaflet.map(mapInfo.mapId, {
      crs:              leaflet.CRS.Simple,
      minZoom:          minZoom,
      maxBounds:        bounds,
      contextmenu:      true,
      contextmenuItems: [
        {
          text:
            '<i class="fa fa-map-marker" aria-hidden="true"></i>&ensp;添加标记',
          callback: function(event) {
            this.mapMarkerEditor.loadMarkerInfo(
              '',
              '',
              event.latlng.lat,
              event.latlng.lng
            );
            this.mapMarkerEditor.show();
          }
        }
      ]
    });
    map.setView(
      [mapInfo.initialLocY, mapInfo.initialLocX],
      mapInfo.initialZoom
    );
    // add controls to map object for easier access
    map.mapInfo = mapInfo;
    map.saveButton = saveButton;
    // setup marker editor
    var mapMarkerEditor = new markerEditor(map);
    mapMarkerEditor.init();
    map.mapMarkerEditor = mapMarkerEditor;
    layer.addTo(map);
    saveButton.addTo(map);
    helpButton.addTo(map);
    toogleMarkerButton.addTo(map);
    // preload icon info then load markers
    preLoadIconInfo(mapInfo);
    setTimeout(function() {
      mapInfo.markersLayerGroup = leaflet.layerGroup();
      for (var m in mapInfo.markerData) {
        addMarker(
          map,
          mapInfo,
          mapInfo.markerData[m].coords.x,
          mapInfo.markerData[m].coords.y,
          mapInfo.markerData[m].tooltip,
          mapInfo.markerData[m].markerImage
        );
      }
      mapInfo.markersLayerGroup.addTo(map);
    }, 1000);
  }
  // preload icon info
  function preLoadIconInfo(mapInfo) {
    for (var m in mapInfo.markerData) {
      if (
        typeof iconMap[mapInfo.markerData[m].markerImage] === 'undefined' &&
        mapInfo.markerData[m].markerImage
      ) {
        // mark the properties which need to be loaded with icon info later on
        iconMap[mapInfo.markerData[m].markerImage] = 'ready';
      }
    }
    var keys = Object.keys(iconMap);
    if (keys.length === 0) {
      return;
    }
    var icons = {
      titles: [],
      names:  []
    };
    for (var i = 0, l = keys.length; i < l; i++) {
      if (iconMap[keys[i]] !== 'ready') {
        continue;
      }
      iconMap[keys[i]] = 'started';
      icons.titles.push('File:' + keys[i]);
      icons.names.push(keys[i]);
    }
    var queryData = {
      action:        'query',
      format:        'json',
      prop:          'imageinfo',
      titles:        icons.titles.join('|'),
      formatversion: '2',
      iiprop:        'url|dimensions'
    };
    requestWithCallback(
      queryData,
      icons,
      function(data, icons) {
        var pages = data.query.pages;
        var normalized = data.query.normalized;
        var namePairs = {};
        for (var i in icons.titles) {
          for (var ii in normalized) {
            if (normalized[ii].from === icons.titles[i]) {
              namePairs[normalized[ii].to] = icons.names[i];
              break;
            }
          }
        }
        for (var p in pages) {
          if (pages.hasOwnProperty(p)) {
            if (pages[p].hasOwnProperty('missing')) {
              continue;
            } else {
              iconMap[namePairs[pages[p].title]] = {
                url:    pages[p].imageinfo[0]['url'],
                width:  pages[p].imageinfo[0]['width'],
                height: pages[p].imageinfo[0]['height']
              };
            }
          }
        }
      },
      'GET',
      function(xhr, icons) {
        for (var i in icons.names) {
          iconMap[icons.names[i]] = 'ready';
        }
      }
    );
  }
  // define an object to handle marker info editing
  function markerEditor(map) {
    this.mapInfo = map.mapInfo;
    this.map = map;
    // editor mode: true - create marker; false - edit marker
    var mode = false;
    var markerCoords = {
      x: undefined,
      y: undefined
    };
    var _marker = undefined;
    var markerImageIdSelector = '#' + map.mapInfo.mapId + '-markerImage';
    var tooltipIdSelector = '#' + map.mapInfo.mapId + '-markerTooltip';
    var confirmIdSelector = '#' + map.mapInfo.mapId + '-confirm';
    var editorIdSelector = '#' + map.mapInfo.mapId + '-markerEditor';
    this.init = function() {
      $('body').append(
        '<div id="' +
          map.mapInfo.mapId +
          '-markerEditor"class="modal fade"data-markerIndex="-1"><div class="modal-dialog"role="document"><div class="modal-content"><div class="modal-header"><h3 class="modal-title"style="font-size:18px;">编辑标记信息</h5></div><div class="modal-body"><form><div class="form-group"><label for="marker-image-name"class="form-control-label">标记图片名:</label><input id="' +
          map.mapInfo.mapId +
          '-markerImage"type="text"class="form-control"placeholder="请输入完整图片名,如“我的图片.png”,留空则使用默认标记"></div><div class="form-group"><label for="marker-tooltip"class="form-control-label">标记提示信息:</label><textarea id="' +
          map.mapInfo.mapId +
          '-markerTooltip"type="text"class="form-control"rows="5"placeholder="此处输入描述文字,文字中可以使用简单的内链,如[[XXX|XX]]和[[XXX]]"></textarea></div></form></div><div class="modal-footer"><button id="' +
          map.mapInfo.mapId +
          '-confirm"type="button"class="btn btn-primary">保存更改</button><button type="button"class="btn btn-secondary"data-dismiss="modal">关闭</button></div></div></div></div>'
      );
      $(confirmIdSelector).click(function(event) {
        var markerImage = $(markerImageIdSelector).val();
        var tooltip = $(tooltipIdSelector).val();
        if (!mode) {
          // edit marker
          if (!tooltip) {
            _marker.unbindPopup();
          } else {
            _marker.bindPopup(escapeInput(tooltip));
          }
          _marker.tooltip = tooltip;
          _marker.markerImage = markerImage;
          loadCustomMarkerIcon(_marker);
        } else {
          // create marker
          addMarker(
            map,
            map.mapInfo,
            markerCoords.x,
            markerCoords.y,
            tooltip,
            markerImage
          );
          map.mapInfo.needSave = true;
          map.saveButton.enable();
        }
        map.mapInfo.markerData = getMarkerData(map.mapInfo);
        map.mapInfo.needSave = true;
        map.saveButton.enable();
        map.saveButton.state('need-save');
        $(editorIdSelector).modal('hide');
      });
    };
    // load existed marker
    this.loadMarker = function(marker) {
      _marker = marker;
      $(markerImageIdSelector).val(marker.markerImage);
      $(tooltipIdSelector).val(marker.tooltip);
      mode = false;
    };
    // load marker info (create marker)
    this.loadMarkerInfo = function(markerImage, markerTooltip, x, y) {
      $(markerImageIdSelector).val(markerImage);
      $(tooltipIdSelector).val(markerTooltip);
      markerCoords.x = x;
      markerCoords.y = y;
      mode = true;
    };
    this.show = function() {
      $(editorIdSelector).modal('show');
    };
    this.hide = function() {
      $(editorIdSelector).modal('hide');
    };
  }
  // load the marker icon set by marker.markerImage; revert to default icon if url is not found
  function loadCustomMarkerIcon(marker) {
    var markerImage = marker.markerImage;
    if (!markerImage) {
      marker.setIcon(defaultIcon);
      return;
    }
    if (typeof iconMap[markerImage] === 'object') {
      var customIcon = leaflet.icon({
        iconUrl:    iconMap[markerImage].url,
        iconSize:   [iconMap[markerImage].width, iconMap[markerImage].height],
        iconAnchor: [
          iconMap[markerImage].width / 2,
          iconMap[markerImage].height / 2
        ],
        popupAnchor:   [0, 0 - iconMap[markerImage].height / 2],
        tooltipAnchor: [iconMap[markerImage].width / 2, 0]
      });
      marker.setIcon(customIcon);
      return;
    }
    var queryData = {
      action:        'query',
      format:        'json',
      prop:          'imageinfo',
      titles:        'File:' + markerImage,
      formatversion: '2',
      iiprop:        'url|dimensions'
    };
    requestWithCallback(queryData, undefined, function(data, undefined) {
      var pages = data.query.pages;
      var pageid;
      for (var p in pages) {
        if (pages.hasOwnProperty(p)) {
          if (pages[p].hasOwnProperty('missing')) {
            marker.setIcon(defaultIcon);
            return;
          } else {
            pageid = p;
            break;
          }
        }
      }
      var markerImageUrl = pages[pageid].imageinfo[0]['url'];
      var markerImageWidth = pages[pageid].imageinfo[0]['width'];
      var markerImageHeight = pages[pageid].imageinfo[0]['height'];
      var customIcon = leaflet.icon({
        iconUrl:       markerImageUrl,
        iconSize:      [markerImageWidth, markerImageHeight],
        iconAnchor:    [markerImageWidth / 2, markerImageHeight / 2],
        popupAnchor:   [0, 0 - markerImageHeight / 2],
        tooltipAnchor: [markerImageWidth / 2, 0]
      });
      marker.setIcon(customIcon);
      iconMap[markerImage] = {
        url:    markerImageUrl,
        width:  markerImageWidth,
        height: markerImageHeight
      };
    });
  }
  function getMarkerData(mapInfo) {
    var markerData = [];
    var layers = mapInfo.markersLayerGroup.getLayers();
    for (var m in layers) {
      markerData.push({
        coords: {
          x: layers[m].getLatLng().lat,
          y: layers[m].getLatLng().lng
        },
        tooltip:     layers[m].tooltip,
        markerImage: layers[m].markerImage
      });
    }
    return markerData;
  }
  function addMarker(map, mapInfo, x, y, tooltip, markerImage) {
    var marker = leaflet.marker([x, y], {
      draggable:               true,
      icon:                    defaultIcon,
      contextmenu:             true,
      contextmenuInheritItems: false,
      contextmenuItems:        [
        {
          text:
            '<i class="fa fa-pencil-square" aria-hidden="true"></i>&ensp;编辑标记',
          callback: function(event) {
            this.mapMarkerEditor.loadMarker(event.relatedTarget);
            this.mapMarkerEditor.show();
          }
        },
        {
          text:     '<i class="fa fa-trash" aria-hidden="true"></i>&ensp;删除标记',
          callback: function(event) {
            removeMarker(this, this.mapInfo, event.relatedTarget);
            this.mapInfo.needSave = true;
            this.saveButton.enable();
          }
        }
      ]
    });
    marker.on('dragend', function(event) {
      mapInfo.markerData = getMarkerData(mapInfo);
      mapInfo.needSave = true;
      map.saveButton.enable();
    });
    marker.on('mousedown', function(event) {
      if (event.originalEvent.ctrlKey) {
        mapInfo.markersLayerGroup.addLayer(
          cloneMarker(event.latlng.lat, event.latlng.lng, marker, map, mapInfo)
        );
      }
    });
    if (tooltip) {
      marker.bindPopup(escapeInput(tooltip));
    }
    marker.tooltip = tooltip;
    marker.markerImage = markerImage;
    loadCustomMarkerIcon(marker);
    mapInfo.markersLayerGroup.addLayer(marker);
  }
  function cloneMarker(lat, lng, marker, map, mapInfo) {
    var newMarker = leaflet.marker([lat, lng], marker.options);
    newMarker.on('dragend', function(event) {
      mapInfo.markerData = getMarkerData(mapInfo);
      mapInfo.needSave = true;
      map.saveButton.enable();
    });
    newMarker.on('mousedown', function(event) {
      if (event.originalEvent.ctrlKey) {
        mapInfo.markersLayerGroup.addLayer(
          cloneMarker(event.latlng.lat, event.latlng.lng, marker, map, mapInfo)
        );
      }
    });
    if (marker.tooltip) {
      newMarker.bindPopup(escapeInput(marker.tooltip));
    }
    newMarker.tooltip = marker.tooltip;
    newMarker.markerImage = marker.markerImage;
    return newMarker;
  }
  function removeMarker(map, mapInfo, marker) {
    var index = mapInfo.markersLayerGroup.getLayerId(marker);
    if (index > -1) {
      mapInfo.markersLayerGroup.removeLayer(index);
    }
    mapInfo.markerData = getMarkerData(mapInfo);
  }
  function ucFirst(string) {
    return string.charAt(0).toUpperCase() + string.slice(1);
  }
  function escapeInput(input) {
    return $('<span/>')
      .text(input)
      .wrap('<span/>')
      .parent()
      .text()
      .replace(/\[\[(.+?)\]\]/g, function(match, p1, offset, string) {
        var result = /([^\|]+)\|(.+)/.exec(p1);
        if (result === null) {
          return $('<a/>')
            .attr('href', '/wiki/' + p1)
            .text(p1)
            .wrap('<span/>')
            .parent()
            .html();
        } else {
          return $('<a/>')
            .attr('href', '/wiki/' + result[1])
            .text(result[2])
            .wrap('<span/>')
            .parent()
            .html();
        }
      })
      .replace(/\n/g, '<br/>');
  }
})(L, mediaWiki, jQuery);
//</nowiki>