This is a wiki for a reason. Anyone can contribute. If you see something that is inaccurate or can be improved, don't ask that it be fixed--just improve it.
[ Disclaimer, Create new user --- Wiki markup help, Install P99 ]

Difference between revisions of "MediaWiki:LocMaps.js"

From Project 1999 Wiki
Jump to: navigation, search
 
(93 intermediate revisions by one user not shown)
Line 1: Line 1:
 
try {
 
try {
(function() {
+
  (function() {
  
  // TODO: Add support for cropping maps that have multiple maps.
+
    var buildImgFromFileName = function(fileName) {
  // For instance, the following CSS styles show only one of the three maps for
+
      return '<img src="/images/' + fileName.replace(/ /g, '_') + '">'
  // Erudin Palace:
+
    };
  // background-image:url(/images/Erudinpalace.jpg);
+
  // background-position: 250px 0px; width: 275px; height: 272px
+
  // Figure out how to make the Xs line up with those styles added
+
  
  var locIsWithinAlternateData = function(loc, alternateData) {
+
     // TODO: Add support for cropping maps that have multiple maps.
     return loc.x < alternateData.maxX &&
+
     // For instance, the following CSS styles show only one of the three maps for
     loc.x > alternateData.minX &&
+
     // Erudin Palace:
     loc.y < alternateData.maxY &&
+
     // background-image:url(/images/Erudinpalace.jpg);  
     loc.y > alternateData.minY;
+
    // background-position: 250px 0px; width: 275px; height: 272px
  };
+
    // Figure out how to make the Xs line up with those styles added
  
 +
    var locIsWithinAlternateData = function(loc, alternateData) {
 +
      return loc.x < alternateData.maxX &&
 +
      loc.x > alternateData.minX &&
 +
      loc.y < alternateData.maxY &&
 +
      loc.y > alternateData.minY;
 +
    };
  
  /**
 
  * Determines if the provided name contains any of the provided words
 
  */
 
  // TODO: Add a some polyfill to make this a lot simpler
 
  var containsAny = function(name/* word1, word2, etc. */) {
 
    for(var arg = 1; arg < arguments.length; ++ arg) {
 
      var word = arguments[arg];
 
      if (name.includes(word)) return true;
 
    }
 
    return false;
 
  };
 
  
  var containsAnyNumber = function(name, number, suffix) {
+
    /**
    return containsAny.call(null, name,
+
    * Determines if the provided name contains any of the provided words
    number + suffix,
+
    */
    'level ' + number,
+
    // TODO: Add a some polyfill to make this a lot simpler
    'level: ' + number
+
    var containsAny = function(name/* word1, word2, etc. */) {
    );
+
      for(var arg = 1; arg < arguments.length; ++ arg) {
  };
+
        var word = arguments[arg];
 +
        if (name.includes(word)) return true;
 +
      }
 +
      return false;
 +
    };
  
  var getZoneLevelData = function(levels, text) {
+
    var containsAnyNumber = function(name, number, suffix) {
    // Check for part "level" aliases (eg. "1st floor" vs. "Level One")
+
      return containsAny.call(null, name,  
    text = text.toLowerCase();
+
      number + suffix,
    if (containsAnyNumber(text, 0, 'th') || containsAny(text, 'basement', 'underground')) return levels[0];
+
      'level ' + number,
    if (containsAnyNumber(text, 1, 'st') || containsAny(text, 'one', 'first', 'ground')) return levels[1];
+
      'level: ' + number,
    if (containsAnyNumber(text, 2, 'nd') || containsAny(text, 'two', 'second')) return levels[2];
+
      'floor ' + number,
    if (containsAnyNumber(text, 3, 'rd') || containsAny(text, 'three', 'third')) return levels[3];
+
      'floor: ' + number
    if (containsAnyNumber(text, 4, 'th') || containsAny(text, 'four', 'fourth')) return levels[4];
+
      );
    if (containsAnyNumber(text, 5, 'th') || containsAny(text, 'five', 'fifth')) return levels[5];
+
     };
    if (containsAnyNumber(text, 6, 'th') || containsAny(text, 'six', 'sixth')) return levels[6];
+
    if (containsAnyNumber(text, 7, 'th') || containsAny(text, 'seven', 'seventh')) return levels[7];
+
     if (containsAnyNumber(text, 8, 'th') || containsAny(text, 'eight', 'eighth')) return levels[8];
+
    if (containsAnyNumber(text, 9, 'th') || containsAny(text, 'nine', 'ninth')) return levels[9];
+
  
     var levelKeys = Object.keys(levels);
+
     var getZoneLevelData = function(zone, text) {
    var keyOfLastLevel = levelKeys[levelKeys.length - 1];
+
      var levels = zone.levels;
    if (containsAny(text, 'top')) return levels[keyOfLastLevel];
+
      // Check for part "level" aliases (eg. "1st floor" vs. "Level One")
 +
      text = text.toLowerCase();
 +
      if(levels['3']  && levels['3'].image.includes('crystal')) {
 +
        // Special case for Crystal Caverns
 +
        if (containsAny(text, 'upper')) return levels[3];
 +
        if (containsAny(text, 'lower')) return levels[2];
 +
        if (containsAny(text, 'town', 'coldain')) return levels[0];
 +
      }
  
    // If we still couldn't match, and there is a ground/1st floor, use it
+
      // In most zones "tunnel" = level 0.  But in Kurn's there are two level zeroes (tunnels and basement),
    return levels[1];
+
      // so this next line handles the Kurn's case in particular
  };
+
      if (containsAny(text, 'tunnel') && levels.tunnels) return levels.tunnels;
  
 
+
      if (containsAnyNumber(text, 0, 'th') || containsAny(text, 'basement', 'underground', 'tunnel', 'bear pit', 'bear pits', 'cave', 'caves')) return levels[0];
 +
      if (containsAnyNumber(text, 1, 'st') || containsAny(text, 'one', 'first', 'ground')) return levels[1];
 +
      if (containsAnyNumber(text, 2, 'nd') || containsAny(text, 'two', 'second')) return levels[2];
 +
      if (containsAnyNumber(text, 3, 'rd') || containsAny(text, 'three', 'third')) return levels[3];
 +
      if (containsAnyNumber(text, 4, 'th') || containsAny(text, 'four', 'fourth')) return levels[4];
 +
      if (containsAnyNumber(text, 5, 'th') || containsAny(text, 'five', 'fifth')) return levels[5];
  
  var findZoneData = function(zoneName, locs, nonLocParts) {
+
      // Special case for Tower of Frozen Shadow, which has two level 6 maps
    var zone = zoneData[zoneName];
+
      if (containsAny(text, '6a')) return levels['6A'];
 +
      if (containsAny(text, '6b')) return levels['6B'];
  
  // Handle aliases (eg. "North Ro" instead of "Northern Desert of Ro")
+
      if (containsAnyNumber(text, 6, 'th') || containsAny(text, 'six', 'sixth')) return levels[6];
  if (typeof(zone) === 'string') zone = zoneData[zone];
+
      if (containsAnyNumber(text, 7, 'th') || containsAny(text, 'seven', 'seventh')) return levels[7];
 +
      if (containsAnyNumber(text, 8, 'th') || containsAny(text, 'eight', 'eighth')) return levels[8];
 +
      if (containsAnyNumber(text, 9, 'th') || containsAny(text, 'nine', 'ninth')) return levels[9];
  
  if (!zone) return null;
+
      var levelKeys = Object.keys(levels);
 +
      var keyOfLastLevel = levelKeys[levelKeys.length - 1];
 +
      if (containsAny(text, 'top')) return levels[keyOfLastLevel];
  
  // Handle Multi-Level Zones (with different maps for 1st, 2nd, etc. floor)
+
      // If we still couldn't match, and there is a ground/1st floor, use it
  if (zone.levels) return getZoneLevelData(zone.levels, nonLocParts);
+
      return levels[1];
 +
    };
  
  // Handle Zones with alternate maps on the same level (eg. Kelethin in GFay)
+
   
  if (!zone.alternateMaps) return zone;
+
  
  for (var i = 0; i < zone.alternateMaps.length; i++) {
+
    var findZoneData = function(zoneName, locs, nonLocParts) {
    var alternateData = zone.alternateMaps[i];
+
      // Convert someZone (East) to East someZone
    var allLocsAreWithin = true;
+
      if (zoneName.trim().endsWith(')')) {
    // wish I had ES6 [].every
+
        try {
    $.each(locs, function(i, loc) {
+
          var nameSplit = zoneName.split('(');
      allLocsAreWithin = allLocsAreWithin &&
+
          var newName = nameSplit[0].trim();
      locIsWithinAlternateData(loc, alternateData);
+
          var direction = nameSplit[1].split(')')[0].trim();
    });
+
          if (['east', 'north', 'south', 'west'].includes(direction.toLowerCase())) {
    if (allLocsAreWithin) return alternateData;
+
            zoneName = direction + ' ' + newName;
  };
+
          }
  return zone;
+
        } catch(err){ /* just use name (it likely won't work, but try) */}
};
+
      }
  
var addImageUrl = function(zoneData) {
+
      var zone = zoneData[zoneName];
  zoneData.imageUrl = '/images/' + zoneData.image;
+
  return zoneData;
+
};
+
  
var getZoneData = function(zoneName, locs, nonLocParts) {
+
      // Handle aliases (eg. "North Ro" instead of "Northern Desert of Ro")
  var zoneData = findZoneData(zoneName, locs, nonLocParts);
+
      if (typeof(zone) === 'string') zone = zoneData[zone];
  if (!zoneData) return null;
+
  zoneData.zoneName = zoneName;
+
  return addImageUrl(zoneData);
+
};
+
  
  var build$MapImage = function(zoneData) {
+
      if (!zone) return null;
    return $(
+
      '<img alt="Map of ' + zoneData.zoneName + '" ' +
+
      '    class="thumbborder" ' +
+
      '    height="'+ zoneData.height + '" ' +
+
      '    src="' + zoneData.imageUrl + '" ' +
+
      '    title="Map of ' + zoneData.zoneName + '"' +
+
      '    width="' + zoneData.width + '" ' +
+
      '>'
+
      );
+
  };``
+
  
  var build$XContainer = function() {
+
      // Handle Multi-Level Zones (with different maps for 1st, 2nd, etc. floor)
    return $('<div class="x-container"></div>')
+
      if (zone.levels) {
      .css({ position: 'relative' });
+
        var levelOfZone = getZoneLevelData(zone, nonLocParts);
  }
+
        return levelOfZone;
 +
      }
 +
 
 +
      // Handle Zones with alternate maps on the same level (eg. Kelethin in GFay)
 +
      if (!zone.alternateMaps) return zone;
 +
 
 +
      for (var i = 0; i < zone.alternateMaps.length; i++) {
 +
        var alternateData = zone.alternateMaps[i];
 +
        var allLocsAreWithin = true;
 +
        // wish I had ES6 [].every
 +
        $.each(locs, function(i, loc) {
 +
          allLocsAreWithin = allLocsAreWithin &&
 +
          locIsWithinAlternateData(loc, alternateData);
 +
        });
 +
        if (allLocsAreWithin) return alternateData;
 +
      };
 +
      return zone;
 +
    };
  
   var build$WrappedMap = function(zoneData) {
+
   var addImageUrl = function(zoneData) {
     return build$XContainer().append(build$MapImage(zoneData));
+
     zoneData.imageUrl = '/images/' + zoneData.image;
 +
    return zoneData;
 
   };
 
   };
  
   var build$LightboxFramedMap = function(zoneData) {
+
   var getZoneData = function(zoneName, locs, nonLocParts) {
     var $map = build$WrappedMap(zoneData);
+
     var zoneData = findZoneData(zoneName, locs, nonLocParts);
     return addFramingStyles($map, zoneData.width, zoneData.height)
+
     if (!zoneData) return null;
 +
    zoneData.zoneName = zoneName;
 +
    return addImageUrl(zoneData);
 
   };
 
   };
  
  var addFramingStyles = function($el, width, height) {
+
    var build$MapImage = function(zoneData) {
    return $el.css({
+
      return $(
         left: '50%',
+
         '<img alt="Map of ' + zoneData.zoneName + '" ' +
         marginLeft: '-' + (width / 2) + 'px', // centering
+
         '     class="thumbborder" ' +
         marginTop: '-' + (height / 2) + 'px',
+
        '    height="'+ zoneData.height + '" ' +
         opacity: 1,
+
         '     src="' + zoneData.imageUrl + '" ' +
         top: '50%'
+
         '    title="Map of ' + zoneData.zoneName + '"' +
      });
+
         '     width="' + zoneData.width + '" ' +
  }
+
        '>'
 +
        );
 +
    };``
  
  var build$MapFrame = function() {
+
    var build$XContainer = function() {
    return $('<div class="map-wrapper"></div>')
+
      return $('<div class="x-container" style="position: relative"></div>');
            .css({ position: 'absolute' });
+
    }
  };
+
  
  var removeOpenMap = function(e) {
+
   
    // Do nothing if there's no open map
+
    var build$LightboxFramedMap = function(zoneData) {
    if (!window.$openImg) return false;
+
      var $map = build$XContainer().append(build$MapImage(zoneData));
 +
      return addFramingStyles($map, zoneData.width, zoneData.height)
 +
    };
  
     // Do nothing if the user clicked to open a map, then moused out of an area
+
     var addFramingStyles = function($el, width, height) {
    // (otherwise it can close immediately)
+
      return $el.css({
    var position = window.$openImg.css('position');
+
          left: '50%',
    if (e && e.type === 'mouseleave' && position === 'fixed') return false;
+
          marginLeft: '-' + (width / 2) + 'px', // centering
 +
          marginTop: '-' + (height / 2) + 'px',
 +
          opacity: 1,
 +
          top: '50%'
 +
        });
 +
    }
  
     window.$openImg.remove();
+
     var build$AbsolutePositionFrame = function() {
    return false;
+
      return $('<div style="position: absolute"></div>');
  }
+
    };
  
 +
    var removeOpenImage = function(e) {
 +
      // Do nothing if there's no open map
 +
      if (!window.$openImg) return false;
  
  var build$FullScreenFrame = function() {
+
      // Do nothing if the user clicked to open a map, then moused out of an area
    return build$MapFrame()
+
      // (otherwise it can close immediately)
       .css({
+
       var position = window.$openImg.css('position');
        background: 'rgba(0,0,0,0.8)',
+
      if (e && e.type === 'mouseleave' && position === 'fixed') return false;
        height: '100%',
+
        left: 0,
+
        position: 'fixed',
+
        top: 0,
+
        width: '100%',
+
        zIndex: 4 // one higher than #p-search's 3
+
      })
+
      .click(removeOpenMap);
+
  }
+
  
  var build$FullMap = function(zoneData) {
+
       window.$openImg.remove();
    var $frame = build$FullScreenFrame();
+
       return false;
    if (zoneData) {
+
       $frame.html($build$LightboxFramedMap(zoneData));
+
       $frame.zoneData = zoneData;
+
 
     }
 
     }
    return $frame;
 
  };
 
  
  var build$SmallMap = function(zoneData) {
 
    return build$MapFrame().append(build$WrappedMap(zoneData));
 
  }
 
  
 +
    var build$FullScreenFrame = function() {
 +
      return build$AbsolutePositionFrame()
 +
        .css({
 +
          background: 'rgba(0,0,0,0.8)',
 +
          height: '100%',
 +
          left: 0,
 +
          position: 'fixed',
 +
          top: 0,
 +
          width: '100%',
 +
          zIndex: 4 // one higher than #p-search's 3
 +
        })
 +
        .click(removeOpenImage);
 +
    }
  
var buildX = function(left, top, sizeInEm) {
+
    var build$FullMap = function(zoneData) {
  sizeInEm = sizeInEm || 2.5;
+
      var $frame = build$FullScreenFrame();
  return $('<div class="x">x</div>')
+
      if (zoneData) {
    .css({
+
        $frame.html(build$LightboxFramedMap(zoneData));
      color: 'red',
+
        $frame.zoneData = zoneData;
       fontSize: sizeInEm + 'em',
+
       }
       fontWeight: 'bold',
+
       return $frame;
      left: left,
+
     };
      position: 'absolute',
+
      top: top
+
     })
+
}
+
  
/**
+
    var build$SmallMap = function(zoneData) {
* Draws a red "X" on the map at the provided coordinate
+
      return build$AbsolutePositionFrame().append(
*/
+
        build$XContainer().append(
var addX = function($xContainer, zoneData, x, y, xSize) {
+
          build$MapImage(zoneData)));
  var left = (zoneData.zeroX || 0) + x * -1 * (zoneData.zoomX || 0.1);
+
    }
  var top = (zoneData.zeroY || 0) + y * -1 * (zoneData.zoomY || 0.1);
+
  $xContainer.append(buildX(left, top, xSize));
+
}
+
  
var addXs = function($xContainer, zoneData, locs, xSize) {
+
  var baseXFontSizeInEm = 2;
  $.each(locs, function(i, loc) {
+
    addX($xContainer, zoneData, loc.x, loc.y, xSize);
+
  });
+
}
+
  
 +
  var buildX = function(left, top, sizeInEm) {
 +
    sizeInEm = sizeInEm || baseXFontSizeInEm;
 +
    return $('<div class="x">x</div>')
 +
      .css({
 +
        color: 'red',
 +
        fontSize: sizeInEm + 'em',
 +
        fontWeight: 'bold',
 +
        left: left,
 +
        position: 'absolute',
 +
        top: top
 +
      })
 +
  }
  
var parseLoc = function(locText) {
+
  /**
  if (typeof locText !== 'string') return locText;
+
  * Draws a red "X" on the map at the provided coordinate
 +
  */
 +
  var addX = function($xContainer, zoneData, x, y, xSize) {
 +
    var left = (zoneData.zeroX || 0) + x * -1 * (zoneData.zoomX || 0.1);
 +
    var top = (zoneData.zeroY || 0) + y * -1 * (zoneData.zoomY || 0.1);
 +
    $xContainer.append(buildX(left, top, xSize));
 +
  }
  
   var match = locText.match(/\(? *([\+\-]?\d+\.?\d*), *([\+\-]?\d+\.?\d*)\)?/);
+
   var addXs = function($xContainer, zoneData, locs, xSize) {
  return {x: parseFloat(match[2]), y: parseFloat(match[1]) };
+
    $.each(locs, function(i, loc) {
}
+
      addX($xContainer, zoneData, loc.x, loc.y, xSize);
 +
    });
 +
  }
  
var isLoc = function(locBit) {
+
 
     // If we can't split the string by its comma and find a number on either side, it's not a loc
+
  var parseLoc = function(locText) {
 +
     if (typeof locText !== 'string') return locText;
 
     try {
 
     try {
      return locBit.split(',')[0].match(/\d+/) && locBit.split(',')[1].match(/\d+/);
+
        var match = locText.match(/\(? *([\+\-]?\d+\.?\d*)\s*, *([\+\-]?\d+\.?\d*)\)?/);
 +
        return {x: parseFloat(match[2]), y: parseFloat(match[1]) };
 
     } catch (err) {
 
     } catch (err) {
      return false;
+
        return locText;
 
     }
 
     }
 
   }
 
   }
  
   /**
+
   var isLoc = function(locBit) {
  * Helper function to power parseLocs/extractNonLocText, since they both use
+
      // If we can't split the string by its comma and find a number on either side, it's not a loc
  * the same logic.
+
      try {
  *
+
        return locBit.split(',')[0].match(/\d+/) && locBit.split(',')[1].match(/\d+/);
  * SIDE NOTE: If only the wiki didn't have to use 1999 JS, and could
+
      } catch (err) {
  *            destructure return values with ES2015, the two related functions
+
        return false;
  *            wouldn't even need to exist :(
+
       }
  */
+
     }
  var parseLocString = function(locString) {
+
    var nonLocParts = '';
+
    var bits = locString.split(/([\+\-]?\d+\.?\d*\D*,\D*[\+\-]?\d+\.?\d*)/g);
+
    var relevantBits = bits.filter(function(part) {
+
      // Filter out the non-loc parts, but save them
+
      if (isLoc(part)) return true;
+
       nonLocParts += part; // save as it might be something like "1st floor"
+
    });
+
     var locs = relevantBits.map(parseLoc);
+
    return [locs, nonLocParts];
+
  };
+
  
  /**
+
    /**
  * Extracts all of the locs (objects with x and y properties) from a provided
+
    * Helper function to power parseLocs/extractNonLocText, since they both use
  * string such as:
+
    * the same logic.
  *   "500, 200"
+
    *
  *   "(200, 300), (300, 200)"
+
    * SIDE NOTE: If only the wiki didn't have to use 1999 JS, and could
  * and returns an array containing any locs found.
+
    *           destructure return values with ES2015, the two related functions
  */
+
    *           wouldn't even need to exist :(
  var parseLocs = function(locString) {
+
    */
    return parseLocString(locString)[0];
+
    var parseLocString = function(locString) {
  };
+
      var nonLocParts = '';
 
+
      // Old regex (failed to match Undead Legionaires loc on TT page, but keeping it around unless
  /**
+
      // the new regex has issues
  * Extracts the non-loc parts (eg. "Level 1") from a provided string of text
+
      //var bits = locString.split(/([\+\-]?\d+\.?\d*[^0-9\)]*,\D*[\+\-]?\d+\.?\d*)/g);
  * (presumably from an NPC's location <td> or somewhere similar). Although not
+
      var bits = locString.split(/([\+\-]?\d+\.?\d*\s*,\D*[\+\-]?\d+\.?\d*)/g);
  * actually a part of the loc, this text may still be useful in determining
+
      var relevantBits = bits.filter(function(part) {
  * which map to show for the loc.
+
        // Filter out the non-loc parts, but save them
  */
+
        if (isLoc(part)) return true;
  var extractNonLocText = function(locString) {
+
        nonLocParts += part; // save as it might be something like "1st floor"
    return parseLocString(locString)[1];
+
      });
  };
+
      var locs = relevantBits.map(parseLoc);
 +
      return [locs, nonLocParts];
 +
    };
  
 +
    /**
 +
    * Extracts all of the locs (objects with x and y properties) from a provided
 +
    * string such as:
 +
    *  "500, 200"
 +
    *  "(200, 300), (300, 200)"
 +
    * and returns an array containing any locs found.
 +
    */
 +
    var parseLocs = function(locString) {
 +
      return parseLocString(locString)[0];
 +
    };
 +
   
 +
    /**
 +
    * Extracts the non-loc parts (eg. "Level 1") from a provided string of text
 +
    * (presumably from an NPC's location <td> or somewhere similar). Although not
 +
    * actually a part of the loc, this text may still be useful in determining
 +
    * which map to show for the loc.
 +
    */
 +
    var extractNonLocText = function(locString) {
 +
      return parseLocString(locString)[1];
 +
    };
  
  var showImage = function($img, $container) {
 
    if ($container) {
 
      $img.css({ marginTop: 40 });
 
      var isAboveHalf = $container.position().top < ($('body').height() / 2);
 
      var isLeftOfHalf = $container.position().left < ($('body').width() / 2);
 
      $container.css('position', 'relative')
 
                .prepend($img);
 
      // TODO: Do the same thing with left/right
 
      $img.css({
 
        [isAboveHalf  ? 'top' : 'bottom']: '1.5em',
 
        [isLeftOfHalf  ? 'left' : 'right']: '1.5em'
 
      })
 
    }
 
    else $('body').append($img);
 
  
     window.$openImg = $img;
+
     var showImage = function($img, $container) {
    return $img;
+
      if ($container) {
  };
+
        $img.css({ marginTop: 40 });
 +
        var isAboveHalf = $container.position().top < ($('body').height() / 2);
 +
        var isLeftOfHalf = $container.position().left < ($('body').width() / 2);
 +
        $container.css('position', 'relative')
 +
                  .prepend($img);
 +
        $img.css({
 +
          [isAboveHalf  ? 'top' : 'bottom']: '1.5em',
 +
          [isLeftOfHalf  ? 'left' : 'right']: '1.5em'
 +
        });
 +
        // TODO: Now, check if the map image is off the page, and if so by how much?
 +
        //      Add/subtract that much from the top/left/bottom/right to make sure
 +
        //      the whole map is shown ... but also make sure the link itself isn't covered
 +
      }
 +
      else $('body').append($img);
  
  /**
+
      window.$openImg = $img;
  * Adds a loc map image to the page.
+
      return $img;
  */
+
     };
  var showMapWithLocs = function(zoneData, locs, $container) {
+
    removeOpenMap();
+
    zoneData = addImageUrl(zoneData);
+
     var $map = $container ? build$SmallMap(zoneData) : build$FullMap(zoneData);
+
  
     var $xContainer = $map.find('.x-container');
+
     /**
     $map.find('img').load(function() {
+
    * Adds a loc map image to the page.
       addXs($xContainer, zoneData, locs);
+
    */
    });
+
     var showMapWithLocs = function(zoneData, locs, $container) {
    return showImage($map, $container);
+
       removeOpenImage();
  };
+
     
 +
      zoneData = addImageUrl(zoneData);
 +
      var $map = $container ? build$SmallMap(zoneData) : build$FullMap(zoneData);
  
  // Handle "Where to obtain" rows for spells
+
      var $xContainer = $map.find('.x-container');
  var handleWhereToObtainRows = function() {
+
       var xSize = baseXFontSizeInEm;
    $('table:has(th:contains("Area"))' +
+
       if (locs.length > 5) xSize = 0.8 * baseXFontSizeInEm;
       ':has(th:contains("Location"), th:contains("Loc"))' +
+
       if (locs.length > 15) xSize = 0.5 * baseXFontSizeInEm;
       ':has(th:contains("Zone"))')
+
      $map.find('img').load(function() {
       .each(function(i, table) {
+
         addXs($xContainer, zoneData, locs, xSize);
        var $table = $(table);
+
      });
        var areaColumnIndex = $table.find('th:contains("Area")').index();
+
      return showImage($map, $container);
         var locationColumnIndex =
+
    };
          $table.find('th:contains("Location"), th:contains("Loc")').index();
+
        var zoneColumnIndex = $table.find('th:contains("Zone")').index();
+
  
         $table.find('tr').each(function(i, tr) {
+
    var mapIconHtml = '<img ' +
          var $tr = $(tr);
+
      'alt="Map Icon.png" ' +
          var $loc = $tr.find('td:eq(' + locationColumnIndex + ')');
+
      'height="12" ' +
          var loc = $loc.text().trim();
+
      'src="/images/thumb/Map_Icon.png/12px-Map_Icon.png" ' +
          // Include the area in case it contains relevant non-loc location info
+
      'width="12">';
          // (such as a floor number)
+
    var addLocLinksToTable = function($table, sharedZone, zoneColumnIndex) {
          var nonLoc = $tr.find('td:eq(' + areaColumnIndex + ')').text().trim();
+
      var areaColumnIndex = $table.find('th:contains("Area"), th:contains("Location")').index();
          var zone = $tr.find('td:eq(' + zoneColumnIndex + ')').text().trim();
+
      var locationColumnIndex =
          var $locLink = $('<a href="' + zone + '" ' +
+
         $table.find('th:contains("Location"), th:contains("Loc")').index();
                          '  data-loc="' + nonLoc + ' ' + loc + '" ' +
+
     
                          '  data-zone="' + zone + '"' +
+
 
                          '>' + loc + '</a>');
+
      $table.find('tr').each(function(i, tr) {
          $loc.html($locLink);
+
        var $tr = $(tr);
 +
        var $loc = $tr.find('td:eq(' + locationColumnIndex + ')');
 +
        var loc = $loc.text().trim();
 +
        if (!isLoc(loc)) return; // Non-loc text (eg. "Various") in loc column
 +
       
 +
        // Include the area in case it contains relevant non-loc location info
 +
        // (such as a floor number)
 +
        var nonLoc = $tr.find('td:eq(' + areaColumnIndex + ')').text().trim();
 +
        var zone = sharedZone ||
 +
                  $tr.find('td:eq(' + zoneColumnIndex + ')').text().trim();
 +
                  var $locLink = $('<a class="loc-link" href="' + zone + '" ' +
 +
                  '  data-loc="' + nonLoc + ', ' + loc + '" ' +
 +
                  '  data-zone="' + zone + '"' +
 +
                  '>' + loc + '</a>');
 +
        $loc.html($locLink);
 +
        // If we actually have zone data, add the map icon
 +
        if (getZoneData(zone, loc, nonLoc)) $loc.append(mapIconHtml);
 +
      });
 +
    }
 +
   
 +
    // Handle "Where to obtain" rows for spells
 +
    var handleWhereToObtainRows = function() {
 +
      $('table'+
 +
        ':has(th:contains("Area"))' +
 +
        ':has(th:contains("Location"))'+
 +
        ':has(th:contains("Loc"))' +
 +
        ':has(th:contains("Zone"))'+
 +
        ':not(.zoneTopTable)' +
 +
        ':not(:has(th:contains("Zone Spawn Timer:")))'
 +
      )
 +
        .each(function(i, table) {
 +
          var $table = $(table);
 +
          var zoneColumnIndex = $table.find('th:contains("Zone")').index();
 +
          addLocLinksToTable($table, null, zoneColumnIndex);
 
         });
 
         });
      });
+
    };
  };
+
   
 +
    // Handle Zone NPC Locs
 +
    var handleZoneNPCRows = function() {
 +
      var $npcsInZone = $('i:contains("NPCs that spawn in")');
 +
      if ($npcsInZone.length) {
 +
        var match = $npcsInZone.text().match(/\d+ NPCs that spawn in (.*):/);
 +
        if (!match) return;
  
  var getZoneName = function() {
+
        var zone = match[1];
    try {
+
        var $table = $npcsInZone.parent().next()
      return $('b:contains("Zone:")').parent().text().split('Zone:')[1].trim();
+
        addLocLinksToTable($table, zone);
    } catch (err) {
+
      }
      return null;
+
 
     };
 
     };
  };
 
  
  /**
+
    var getZoneName = function() {
  * Determines whether the page has a "Location" <td> (as all NPC pages do)
+
      try {
  * with parsable locs inside it (as only some do).  Also checks that
+
        return $('b:contains("Zone:")').parents('tr:first').text().split('Zone:')[1].trim();
  * "loc-mapped" zone data exists for the NPC's zone.
+
      } catch (err) {
  */
+
        return null;
  var getZoneDataIfThereIsALocBox = function($locTd, zoneName) {
+
      };
    if (!$locTd.length) return false; // page isn't an NPC page (no loc box)
+
    };
   
+
    var locs = parseLocs($locTd.text());
+
    if (!locs.length) return false; // no locs existed (or could be parsed)
+
  
     var nonLocParts = extractNonLocText($locTd.text());
+
    /**
    return !!getZoneData(zoneName, locs, nonLocParts); // does zone data exist?
+
    * Determines whether the page has a "Location" <td> (as all NPC pages do)
  };
+
    * with parsable locs inside it (as only some do).  Also checks that
 +
    * "loc-mapped" zone data exists for the NPC's zone.
 +
    */
 +
     var getZoneDataIfThereIsALocBox = function($locTd, zoneName) {
 +
      if (!$locTd.length) return false; // page isn't an NPC page (no loc box)
 +
     
 +
      var locs = parseLocs($locTd.text());
 +
      if (!locs.length) return false; // no locs existed (or could be parsed)
  
  var handleLocBoxes = function() {
+
      var nonLocParts = extractNonLocText($locTd.text());
    var zoneName = getZoneName();
+
      return !!getZoneData(zoneName, locs, nonLocParts); // does zone data exist?
    var $locTd = $('b:contains("Location:")').parent();
+
     };
    var zoneData = getZoneDataIfThereIsALocBox($locTd, zoneName);
+
    if (!zoneData) return;
+
   
+
    // Get the mob's loc(s)
+
    var locs = parseLocs($locTd.text());
+
    var nonLocParts = extractNonLocText($locTd.text());
+
   
+
    // Do we have data for that zone's map?
+
    var zoneData = getZoneData(zoneName, locs, nonLocParts);
+
     if (!zoneData) return; // If not, stop here
+
  
     // Add the mouse-enter link
+
     var handleLocBoxes = function() {
    var $link = $(' <a href="#">(Map)</a>')
+
      var zoneName = getZoneName();
      // When it's moused-over, show the map nearby
+
      var $bold = $('b:contains("Location:"), b:contains("Loc:")').not('.zoneTopTable b, .toc b');
      .on('mouseenter', function(e) {
+
      if(!$bold.length) return;
        // If there is already a map "open" (because of a click), do nothing
+
        if (window.$openImg && window.$openImg.parent().length) return false;
+
  
        var $map = showMapWithLocs(zoneData, locs, $locTd);
+
      var label = $bold.text().trim();
        $map.parent().one('mouseleave', removeOpenMap);
+
       var labelNoColon = label.substr(0, label.length - 1);
       })
+
      // When it's clicked, show the map full-screen/lightbox style
+
      .on('click', function(e) {
+
        showMapWithLocs(zoneData, locs);
+
      });
+
  
    // $locTd.find('b').html($('<span>Location </span>').append($link).append('<span>:</span>'));
+
      var $locTd = $bold.parent();
    $locTd
+
      // The TD can either have the label and the locs, or there could
       .find('b')
+
      // be one TD for the label, and a sibling for the loc
      .html($('<span>Location </span>')
+
      var isLabelTd = $locTd.text().trim().endsWith(':');
      .append($link)
+
       if (isLabelTd) $locTd = $locTd.closest('tr').children('td:last');
      .append('<span>:</span>'));
+
  };
+
  
  // Setup event listeners (code "starts" here)
+
      var zoneData = getZoneDataIfThereIsALocBox($locTd, zoneName);
  var startLocFunctionality = function() {
+
      if (!zoneData) return;
    handleWhereToObtainRows();
+
     
    handleLocBoxes();
+
      // Get the mob's loc(s)
  };
+
      var locs = parseLocs($locTd.text());
 +
      var nonLocParts = extractNonLocText($locTd.text());
 +
     
 +
      // Do we have data for that zone's map?
 +
      var zoneData = getZoneData(zoneName, locs, nonLocParts);
 +
      if (!zoneData) return; // If not, stop here
  
  // Sometimes zoneData isn't ready when this code is, so retry for 10 secs in hope that it becomes available
+
      // Add the mouse-enter link
  var tries = 10;
+
      var $link = $(' <a href="#">(Map)</a>')
  var timeBetweenTries = 1000; // 1 second
+
        // When it's moused-over, show the map nearby
  var interval = window.setInterval(function() {
+
        .on('mouseenter', function(e) {
    if (window.zoneData) {
+
          // If there is already a map "open" (because of a click), do nothing
      window.clearInterval(interval);
+
          if (window.$openImg && window.$openImg.parent().length) return false;
      startLocFunctionality();
+
    } else {
+
      tries -= 1;
+
      if (!tries) window.clearInterval(interval); // Failure :(
+
    }
+
  }, timeBetweenTries);
+
  
  var alertNoData = function(zone) {
+
          var $map = showMapWithLocs(zoneData, locs, $locTd);
    alert('We\'re sorry, but ' + zone + ' has not been loc mapped yet ... ' +
+
          $map.parent().one('mouseleave', removeOpenImage);
          'please see "/Loc_Maps" for more information.');  
+
        })
  }
+
        // When it's clicked, show the map full-screen/lightbox style
 +
        .on('click', function(e) {
 +
          showMapWithLocs(zoneData, locs);
 +
        });
  
  var showMapForData = function(data, $target) {
+
      $bold
    if (!data.loc || ! data.zone) return;
+
        .html($('<span>' + labelNoColon + ' </span>')
 +
        .append($link)
 +
        .append('<span>:</span>'));
 +
    };
  
     var locs = [parseLoc(data.loc)];
+
     // Setup event listeners (code "starts" here)
     var zoneData = getZoneData(data.zone, locs);
+
     var startLocFunctionality = function() {
    if (!zoneData) {
+
      handleWhereToObtainRows();
       // If it was a click and not a mouseover
+
       handleZoneNPCRows();
      if (!$target) alertNoData(data.zone);
+
       handleLocBoxes();
       return;
+
    };
    }
+
    showMapWithLocs(zoneData, locs, $target);
+
  };
+
  
  // TODO: this is in here because fashion links and loc links share global
+
    // Sometimes zoneData isn't ready when this code is, so retry for 10 secs in hope that it becomes available
  // event watchers, but this file needs to be renamed now
+
    var tries = 10;
  var showFashionForData = function(data) {
+
    var timeBetweenTries = 1000; // 1 second
    if (!data.file) return;
+
    var interval = window.setInterval(function() {
    var $img = addFramingStyles($('<img src="/images/' + data.file + '" />'));
+
      if (window.zoneData) {
    var $div = $('<div style="position:relative"></div>')
+
        window.clearInterval(interval);
                  .html($img)
+
        startLocFunctionality();
    var $framedImage = build$FullScreenFrame().html($div)
+
      } else {
    showImage($framedImage);
+
        tries -= 1;
  };
+
        if (!tries) window.clearInterval(interval); // Failure :(
 +
      }
 +
    }, timeBetweenTries);
  
  // Hook up loc-link events
+
     var showMapForData = function(data, $target) {
  $('body')
+
       if (!data.loc || ! data.zone) return;
     .on('click', '.loc-link, .fashion-link', function() {
+
      var data = $(this).data();
+
      showMapForData(data);
+
      showFashionForData(data);
+
      return false;
+
    })
+
    .on('mouseenter', '.loc-link', function(e) {
+
      var $this = $(e.target).on('mouseleave', removeOpenMap);
+
       showMapForData($(this).data(), $this);
+
    });
+
  
   
+
      var locs = parseLocString(data.loc)[0];
  // *** HELPER FUNCTIONS ***
+
      var nonLocParts = parseLocString(data.loc)[1];
  // Define two helper functions for building new zone definitions
+
      var zoneData = getZoneData(data.zone, locs, nonLocParts);
 +
      if (!zoneData) {
 +
        // If it was a click and not a mouseover
 +
        if ($target) return false;
  
  // 1) Use this function to find the correct 0,0 point
+
        return confirm('We\'re sorry, but ' + data.zone + ' has not been loc mapped yet ... ' +
  window.testZero = window.testZeroZero = function(zoneData) {
+
          'please see "/Loc_Maps" for more information.  You can now click "OK" to be taken ' +
     removeOpenMap(); // test functions don't clean up properly
+
          'to the zone\'s page instead, where you can use its map as a guide, or "Cancel" to stay here.');
    var locs = [{ x: 0, y: 0 }];
+
      }
    showMapWithLocs(zoneData, locs);
+
      showMapWithLocs(zoneData, locs, $target);
  };
+
     };
 +
  // TODO: Add a new class/event handler/template to trigger this
 +
  var addCharacterToMap = function(data, $target) {
 +
      if (!data.loc || ! data.zone) return;
  
 +
      var locs = parseLocString(data.loc)[0];
 +
      var nonLocParts = parseLocString(data.loc)[1];
 +
      var zoneData = getZoneData(data.zone, locs, nonLocParts);
 +
      if (!zoneData) {
 +
        // If it was a click and not a mouseover
 +
        if ($target) return false;
  
  // 2) Use this function to generate a grid of alignment of X's
+
        alertNoData(data.zone);
  window.testGrid = function(zoneData) {
+
         return true;
    removeOpenMap(); // test functions don't clean up properly
+
    var $locTd = $('b:contains("Location:")').parent();
+
    var locs = [];
+
    for (var x = zoneData.maxX; x >= zoneData.minX; x -= zoneData.interval) {
+
      for (var y = zoneData.maxY; y >= zoneData.minY; y -= zoneData.interval) {
+
         locs.push({ x: x, y: y });
+
 
       }
 
       }
 +
      // TODO: do next line, but replace $target with a query for the existing map on the page
 +
      // Also, instead of making an X, make a number that's provided (via link's data attr)
 +
      // showMapWithLocs(zoneData, locs, $target);
 
     };
 
     };
    showMapWithLocs(zoneData, locs);
 
  };
 
  
})();
+
    // TODO: this is in here because fashion links and loc links share global
} catch (err) {
+
    // event watchers, but this file needs to be renamed now
  console.error(err); // If anything goes wrong, move on but log the error
+
 
}
+
    /**
 +
    * Shows an image (triggered by clicking on a link, such
 +
    * as a thumbnail or a fashion show link, which contains
 +
    * data about the file's URL).
 +
    */
 +
    var showImageFromData = function(data, $container) {
 +
      if (!data.file) return;
 +
      var $img = $(buildImgFromFileName(data.file));
 +
      var $div = $('<div class="framed-image"></div>')
 +
                    .html($img)
 +
      var $framedImage;
 +
      if ($container) {
 +
        var $outerFrame = build$AbsolutePositionFrame();
 +
        var $innerFrame = $('<div style="position:relative"></div>');
 +
        $framedImage = $outerFrame.append($innerFrame.append($div));
 +
      } else {
 +
        $framedImage = build$FullScreenFrame().html($div);
 +
      }
 +
      showImage($framedImage, $container);
 +
    };
 +
 
 +
    // Hook up loc-link events
 +
    $('body')
 +
      .on('click', '.loc-link, .fashion-link, .thumbnail-link', function() {
 +
        removeOpenImage(); // get rid of any mouseover images first
 +
        var data = $(this).data();
 +
        var theClickedLinkShouldRedirect = showMapForData(data) || showImageFromData(data);
 +
        return theClickedLinkShouldRedirect || false;
 +
      })
 +
      // Temporarily (?) disable mouseover showing of images
 +
      //.on('mouseenter', '.loc-link, .fashion-link, .thumbnail-link', function(e) {
 +
      //  var $this = $(e.target).on('mouseleave', removeOpenImage);
 +
      //  var data = $(this).data();
 +
      //  showMapForData(data, $this);
 +
      //  showFashionForData(data, $this);
 +
      //});
 +
 
 +
     
 +
    // *** HELPER FUNCTIONS ***
 +
    // Define two helper functions for building new zone definitions
 +
 
 +
    // 1) Use this function to find the correct 0,0 point
 +
    window.testZero = window.testZeroZero = function(zoneData) {
 +
      removeOpenImage(); // test functions don't clean up properly
 +
      var locs = [{ x: 0, y: 0 }];
 +
      showMapWithLocs(zoneData, locs);
 +
    };
 +
 
 +
 
 +
    // 2) Use this function to generate a grid of alignment of X's
 +
    window.testGrid = function(zoneData) {
 +
      removeOpenImage(); // test functions don't clean up properly
 +
      var $locTd = $('b:contains("Location:")').parent();
 +
      var locs = [];
 +
      for (var x = zoneData.maxX; x >= zoneData.minX; x -= zoneData.interval) {
 +
        for (var y = zoneData.maxY; y >= zoneData.minY; y -= zoneData.interval) {
 +
          locs.push({ x: x, y: y });
 +
        }
 +
      };
 +
      showMapWithLocs(zoneData, locs);
 +
    };
 +
 
 +
  })();
 +
  } catch (err) {
 +
    console.error(err); // If anything goes wrong, move on but log the error
 +
  }

Latest revision as of 20:01, 30 November 2025

try {
  (function() {

    var buildImgFromFileName = function(fileName) {
      return '<img src="/images/' + fileName.replace(/ /g, '_') + '">'
    };

    // TODO: Add support for cropping maps that have multiple maps.
    // For instance, the following CSS styles show only one of the three maps for 
    // Erudin Palace:
    // background-image:url(/images/Erudinpalace.jpg); 
    // background-position: 250px 0px; width: 275px; height: 272px
    // Figure out how to make the Xs line up with those styles added

    var locIsWithinAlternateData = function(loc, alternateData) {
      return loc.x < alternateData.maxX &&
      loc.x > alternateData.minX &&
      loc.y < alternateData.maxY &&
      loc.y > alternateData.minY;
    };


    /**
     * Determines if the provided name contains any of the provided words
     */
    // TODO: Add a some polyfill to make this a lot simpler
    var containsAny = function(name/* word1, word2, etc. */) {
      for(var arg = 1; arg < arguments.length; ++ arg) {
        var word = arguments[arg];
        if (name.includes(word)) return true;
      }
      return false;
    };

    var containsAnyNumber = function(name, number, suffix) {
      return containsAny.call(null, name, 
       number + suffix,
       'level ' + number,
       'level: ' + number,
       'floor ' + number,
       'floor: ' + number
       );
    };

    var getZoneLevelData = function(zone, text) {
      var levels = zone.levels;
      // Check for part "level" aliases (eg. "1st floor" vs. "Level One")
      text = text.toLowerCase();
      if(levels['3']  && levels['3'].image.includes('crystal')) {
        // Special case for Crystal Caverns
        if (containsAny(text, 'upper')) return levels[3];
        if (containsAny(text, 'lower')) return levels[2];
        if (containsAny(text, 'town', 'coldain')) return levels[0];
      }

      // In most zones "tunnel" = level 0.  But in Kurn's there are two level zeroes (tunnels and basement),
      // so this next line handles the Kurn's case in particular
      if (containsAny(text, 'tunnel') && levels.tunnels) return levels.tunnels;

      if (containsAnyNumber(text, 0, 'th') || containsAny(text, 'basement', 'underground', 'tunnel', 'bear pit', 'bear pits', 'cave', 'caves')) return levels[0];
      if (containsAnyNumber(text, 1, 'st') || containsAny(text, 'one', 'first', 'ground')) return levels[1];
      if (containsAnyNumber(text, 2, 'nd') || containsAny(text, 'two', 'second')) return levels[2];
      if (containsAnyNumber(text, 3, 'rd') || containsAny(text, 'three', 'third')) return levels[3];
      if (containsAnyNumber(text, 4, 'th') || containsAny(text, 'four', 'fourth')) return levels[4];
      if (containsAnyNumber(text, 5, 'th') || containsAny(text, 'five', 'fifth')) return levels[5];

      // Special case for Tower of Frozen Shadow, which has two level 6 maps
      if (containsAny(text, '6a')) return levels['6A'];
      if (containsAny(text, '6b')) return levels['6B'];

      if (containsAnyNumber(text, 6, 'th') || containsAny(text, 'six', 'sixth')) return levels[6];
      if (containsAnyNumber(text, 7, 'th') || containsAny(text, 'seven', 'seventh')) return levels[7];
      if (containsAnyNumber(text, 8, 'th') || containsAny(text, 'eight', 'eighth')) return levels[8];
      if (containsAnyNumber(text, 9, 'th') || containsAny(text, 'nine', 'ninth')) return levels[9];

      var levelKeys = Object.keys(levels);
      var keyOfLastLevel = levelKeys[levelKeys.length - 1];
      if (containsAny(text, 'top')) return levels[keyOfLastLevel];

      // If we still couldn't match, and there is a ground/1st floor, use it
      return levels[1];
    };

    

    var findZoneData = function(zoneName, locs, nonLocParts) {
      // Convert someZone (East) to East someZone
      if (zoneName.trim().endsWith(')')) {
        try {
          var nameSplit = zoneName.split('(');
          var newName = nameSplit[0].trim();
          var direction = nameSplit[1].split(')')[0].trim();
          if (['east', 'north', 'south', 'west'].includes(direction.toLowerCase())) {
            zoneName = direction + ' ' + newName;
          }
        } catch(err){ /* just use name (it likely won't work, but try) */}
      }

      var zone = zoneData[zoneName];

      // Handle aliases (eg. "North Ro" instead of "Northern Desert of Ro")
      if (typeof(zone) === 'string') zone = zoneData[zone];

      if (!zone) return null;

      // Handle Multi-Level Zones (with different maps for 1st, 2nd, etc. floor)
      if (zone.levels) {
        var levelOfZone = getZoneLevelData(zone, nonLocParts);
        return levelOfZone;
      }

      // Handle Zones with alternate maps on the same level (eg. Kelethin in GFay)
      if (!zone.alternateMaps) return zone;

      for (var i = 0; i < zone.alternateMaps.length; i++) {
        var alternateData = zone.alternateMaps[i];
        var allLocsAreWithin = true;
        // wish I had ES6 [].every
        $.each(locs, function(i, loc) {
          allLocsAreWithin = allLocsAreWithin && 
          locIsWithinAlternateData(loc, alternateData);
        });
        if (allLocsAreWithin) return alternateData;
      };
      return zone;
    };

  var addImageUrl = function(zoneData) {
    zoneData.imageUrl = '/images/' + zoneData.image;
    return zoneData;
  };

  var getZoneData = function(zoneName, locs, nonLocParts) {
    var zoneData = findZoneData(zoneName, locs, nonLocParts);
    if (!zoneData) return null;
    zoneData.zoneName = zoneName;
    return addImageUrl(zoneData);
  };

    var build$MapImage = function(zoneData) {
      return $(
        '<img alt="Map of ' + zoneData.zoneName + '" ' +
        '     class="thumbborder" ' +
        '     height="'+ zoneData.height + '" ' +
        '     src="' + zoneData.imageUrl + '" ' +
        '     title="Map of ' + zoneData.zoneName + '"' +
        '     width="' + zoneData.width + '" ' +
        '>'
        );
    };``

    var build$XContainer = function() {
      return $('<div class="x-container" style="position: relative"></div>');
    }

    
    var build$LightboxFramedMap = function(zoneData) {
      var $map = build$XContainer().append(build$MapImage(zoneData));
      return addFramingStyles($map, zoneData.width, zoneData.height)
    };

    var addFramingStyles = function($el, width, height) {
      return $el.css({
          left: '50%',
          marginLeft: '-' + (width / 2) + 'px', // centering
          marginTop: '-' + (height / 2) + 'px',
          opacity: 1,
          top: '50%'
        });
    }

    var build$AbsolutePositionFrame = function() {
      return $('<div style="position: absolute"></div>');
    };

    var removeOpenImage = function(e) {
      // Do nothing if there's no open map
      if (!window.$openImg) return false;

      // Do nothing if the user clicked to open a map, then moused out of an area
      // (otherwise it can close immediately)
      var position = window.$openImg.css('position');
      if (e && e.type === 'mouseleave' && position === 'fixed') return false;

      window.$openImg.remove();
      return false;
    }


    var build$FullScreenFrame = function() {
      return build$AbsolutePositionFrame()
        .css({
          background: 'rgba(0,0,0,0.8)',
          height: '100%',
          left: 0,
          position: 'fixed',
          top: 0,
          width: '100%',
          zIndex: 4 // one higher than #p-search's 3
        })
        .click(removeOpenImage);
    }

    var build$FullMap = function(zoneData) {
      var $frame = build$FullScreenFrame();
      if (zoneData) {
        $frame.html(build$LightboxFramedMap(zoneData));
        $frame.zoneData = zoneData;
      }
      return $frame;
    };

    var build$SmallMap = function(zoneData) {
      return build$AbsolutePositionFrame().append(
        build$XContainer().append(
          build$MapImage(zoneData)));
    }

  var baseXFontSizeInEm = 2;

  var buildX = function(left, top, sizeInEm) {
    sizeInEm = sizeInEm || baseXFontSizeInEm;
    return $('<div class="x">x</div>')
      .css({
        color: 'red',
        fontSize: sizeInEm + 'em',
        fontWeight: 'bold',
        left: left,
        position: 'absolute',
        top: top
      })
  }

  /**
   * Draws a red "X" on the map at the provided coordinate
   */
  var addX = function($xContainer, zoneData, x, y, xSize) {
    var left = (zoneData.zeroX || 0) + x * -1 * (zoneData.zoomX || 0.1);
    var top = (zoneData.zeroY || 0) + y * -1 * (zoneData.zoomY || 0.1);
    $xContainer.append(buildX(left, top, xSize));
  }

  var addXs = function($xContainer, zoneData, locs, xSize) {
    $.each(locs, function(i, loc) {
      addX($xContainer, zoneData, loc.x, loc.y, xSize);
    });
  }


  var parseLoc = function(locText) {
    if (typeof locText !== 'string') return locText;
    try {
        var match = locText.match(/\(? *([\+\-]?\d+\.?\d*)\s*, *([\+\-]?\d+\.?\d*)\)?/);
        return {x: parseFloat(match[2]), y: parseFloat(match[1]) };
    } catch (err) {
        return locText;
    }
  }

  var isLoc = function(locBit) {
      // If we can't split the string by its comma and find a number on either side, it's not a loc
      try {
        return locBit.split(',')[0].match(/\d+/) && locBit.split(',')[1].match(/\d+/);
      } catch (err) {
        return false;
      }
    }

    /**
     * Helper function to power parseLocs/extractNonLocText, since they both use
     * the same logic.
     *
     * SIDE NOTE: If only the wiki didn't have to use 1999 JS, and could
     *            destructure return values with ES2015, the two related functions
     *            wouldn't even need to exist :(
     */
    var parseLocString = function(locString) {
      var nonLocParts = '';
      // Old regex (failed to match Undead Legionaires loc on TT page, but keeping it around unless
      // the new regex has issues
      //var bits = locString.split(/([\+\-]?\d+\.?\d*[^0-9\)]*,\D*[\+\-]?\d+\.?\d*)/g);
      var bits = locString.split(/([\+\-]?\d+\.?\d*\s*,\D*[\+\-]?\d+\.?\d*)/g);
      var relevantBits = bits.filter(function(part) {
        // Filter out the non-loc parts, but save them
        if (isLoc(part)) return true;
        nonLocParts += part; // save as it might be something like "1st floor"
      });
      var locs = relevantBits.map(parseLoc);
      return [locs, nonLocParts];
    };

    /**
     * Extracts all of the locs (objects with x and y properties) from a provided
     * string such as:
     *   "500, 200"
     *   "(200, 300), (300, 200)"
     * and returns an array containing any locs found.
     */
     var parseLocs = function(locString) {
      return parseLocString(locString)[0];
    };
    
    /**
     * Extracts the non-loc parts (eg. "Level 1") from a provided string of text
     * (presumably from an NPC's location <td> or somewhere similar). Although not
     * actually a part of the loc, this text may still be useful in determining
     * which map to show for the loc.
     */
    var extractNonLocText = function(locString) {
      return parseLocString(locString)[1];
    };


    var showImage = function($img, $container) {
      if ($container) {
        $img.css({ marginTop: 40 });
        var isAboveHalf = $container.position().top < ($('body').height() / 2);
        var isLeftOfHalf = $container.position().left < ($('body').width() / 2);
        $container.css('position', 'relative')
                  .prepend($img);
        $img.css({ 
          [isAboveHalf  ? 'top' : 'bottom']: '1.5em', 
          [isLeftOfHalf  ? 'left' : 'right']: '1.5em' 
        });
        // TODO: Now, check if the map image is off the page, and if so by how much?
        //       Add/subtract that much from the top/left/bottom/right to make sure 
        //       the whole map is shown ... but also make sure the link itself isn't covered
      }
      else $('body').append($img);

      window.$openImg = $img;
      return $img;
    };

    /**
     * Adds a loc map image to the page.
     */
    var showMapWithLocs = function(zoneData, locs, $container) {
      removeOpenImage();
      
      zoneData = addImageUrl(zoneData);
      var $map = $container ? build$SmallMap(zoneData) : build$FullMap(zoneData);

      var $xContainer = $map.find('.x-container');
      var xSize = baseXFontSizeInEm;
      if (locs.length > 5) xSize = 0.8 * baseXFontSizeInEm;
      if (locs.length > 15) xSize = 0.5 * baseXFontSizeInEm;
      $map.find('img').load(function() {
        addXs($xContainer, zoneData, locs, xSize);
      });
      return showImage($map, $container);
    };

    var mapIconHtml = '<img ' +
      'alt="Map Icon.png" ' +
      'height="12" ' +
      'src="/images/thumb/Map_Icon.png/12px-Map_Icon.png" ' +
      'width="12">';
    var addLocLinksToTable = function($table, sharedZone, zoneColumnIndex) {
      var areaColumnIndex = $table.find('th:contains("Area"), th:contains("Location")').index();
      var locationColumnIndex =
        $table.find('th:contains("Location"), th:contains("Loc")').index();
      

      $table.find('tr').each(function(i, tr) {
        var $tr = $(tr);
        var $loc = $tr.find('td:eq(' + locationColumnIndex + ')');
        var loc = $loc.text().trim();
        if (!isLoc(loc)) return; // Non-loc text (eg. "Various") in loc column
        
        // Include the area in case it contains relevant non-loc location info
        // (such as a floor number)
        var nonLoc = $tr.find('td:eq(' + areaColumnIndex + ')').text().trim();
        var zone = sharedZone ||
                   $tr.find('td:eq(' + zoneColumnIndex + ')').text().trim();
                   var $locLink = $('<a class="loc-link" href="' + zone + '" ' +
                   '   data-loc="' + nonLoc + ', ' + loc + '" ' +
                   '   data-zone="' + zone + '"' +
                   '>' + loc + '</a>');
        $loc.html($locLink);
        // If we actually have zone data, add the map icon
        if (getZoneData(zone, loc, nonLoc)) $loc.append(mapIconHtml);
      });
    }
    
    // Handle "Where to obtain" rows for spells
    var handleWhereToObtainRows = function() {
      $('table'+
        ':has(th:contains("Area"))' +
        ':has(th:contains("Location"))'+
        ':has(th:contains("Loc"))' +
        ':has(th:contains("Zone"))'+
        ':not(.zoneTopTable)' +
        ':not(:has(th:contains("Zone Spawn Timer:")))'
      )
        .each(function(i, table) {
          var $table = $(table);
          var zoneColumnIndex = $table.find('th:contains("Zone")').index();
          addLocLinksToTable($table, null, zoneColumnIndex);
        });
    };
    
    // Handle Zone NPC Locs
    var handleZoneNPCRows = function() {
      var $npcsInZone = $('i:contains("NPCs that spawn in")');
      if ($npcsInZone.length) {
        var match = $npcsInZone.text().match(/\d+ NPCs that spawn in (.*):/);
        if (!match) return;

        var zone = match[1];
        var $table = $npcsInZone.parent().next()
        addLocLinksToTable($table, zone);
      }
    };

    var getZoneName = function() {
      try {
        return $('b:contains("Zone:")').parents('tr:first').text().split('Zone:')[1].trim();
      } catch (err) {
        return null;
      };
    };

    /**
     * Determines whether the page has a "Location" <td> (as all NPC pages do)
     * with parsable locs inside it (as only some do).  Also checks that
     * "loc-mapped" zone data exists for the NPC's zone.
     */
    var getZoneDataIfThereIsALocBox = function($locTd, zoneName) {
      if (!$locTd.length) return false; // page isn't an NPC page (no loc box)
      
      var locs = parseLocs($locTd.text());
      if (!locs.length) return false; // no locs existed (or could be parsed)

      var nonLocParts = extractNonLocText($locTd.text());
      return !!getZoneData(zoneName, locs, nonLocParts); // does zone data exist?
    };

    var handleLocBoxes = function() {
      var zoneName = getZoneName();
      var $bold = $('b:contains("Location:"), b:contains("Loc:")').not('.zoneTopTable b, .toc b');
      if(!$bold.length) return;

      var label = $bold.text().trim();
      var labelNoColon = label.substr(0, label.length - 1);

      var $locTd = $bold.parent();
      // The TD can either have the label and the locs, or there could
      // be one TD for the label, and a sibling for the loc
      var isLabelTd = $locTd.text().trim().endsWith(':');
      if (isLabelTd) $locTd = $locTd.closest('tr').children('td:last');

      var zoneData = getZoneDataIfThereIsALocBox($locTd, zoneName);
      if (!zoneData) return;
      
      // Get the mob's loc(s)
      var locs = parseLocs($locTd.text());
      var nonLocParts = extractNonLocText($locTd.text());
      
      // Do we have data for that zone's map?
      var zoneData = getZoneData(zoneName, locs, nonLocParts);
      if (!zoneData) return;  // If not, stop here

      // Add the mouse-enter link
      var $link = $(' <a href="#">(Map)</a>')
        // When it's moused-over, show the map nearby
        .on('mouseenter', function(e) {
          // If there is already a map "open" (because of a click), do nothing
          if (window.$openImg && window.$openImg.parent().length) return false;

          var $map = showMapWithLocs(zoneData, locs, $locTd);
          $map.parent().one('mouseleave', removeOpenImage);
        })
        // When it's clicked, show the map full-screen/lightbox style
        .on('click', function(e) {
          showMapWithLocs(zoneData, locs);
        });

      $bold
        .html($('<span>' + labelNoColon + ' </span>')
        .append($link)
        .append('<span>:</span>'));
    };

    // Setup event listeners (code "starts" here)
    var startLocFunctionality = function() {
      handleWhereToObtainRows();
      handleZoneNPCRows();
      handleLocBoxes();
    };

    // Sometimes zoneData isn't ready when this code is, so retry for 10 secs in hope that it becomes available
    var tries = 10;
    var timeBetweenTries = 1000; // 1 second
    var interval = window.setInterval(function() {
      if (window.zoneData) {
         window.clearInterval(interval);
         startLocFunctionality();
      } else {
        tries -= 1;
        if (!tries) window.clearInterval(interval); // Failure :(
      }
    }, timeBetweenTries);

    var showMapForData = function(data, $target) {
      if (!data.loc || ! data.zone) return;

      var locs = parseLocString(data.loc)[0];
      var nonLocParts = parseLocString(data.loc)[1];
      var zoneData = getZoneData(data.zone, locs, nonLocParts);
      if (!zoneData) {
        // If it was a click and not a mouseover
        if ($target) return false;

        return confirm('We\'re sorry, but ' + data.zone + ' has not been loc mapped yet ... ' +
          'please see "/Loc_Maps" for more information.  You can now click "OK" to be taken ' +
          'to the zone\'s page instead, where you can use its map as a guide, or "Cancel" to stay here.'); 
      }
      showMapWithLocs(zoneData, locs, $target);
    };
   // TODO: Add a new class/event handler/template to trigger this
   var addCharacterToMap = function(data, $target) {
      if (!data.loc || ! data.zone) return;

      var locs = parseLocString(data.loc)[0];
      var nonLocParts = parseLocString(data.loc)[1];
      var zoneData = getZoneData(data.zone, locs, nonLocParts);
      if (!zoneData) {
        // If it was a click and not a mouseover
        if ($target) return false;

        alertNoData(data.zone);
        return true;
      }
      // TODO: do next line, but replace $target with a query for the existing map on the page
      // Also, instead of making an X, make a number that's provided (via link's data attr)
      // showMapWithLocs(zoneData, locs, $target);
    };

    // TODO: this is in here because fashion links and loc links share global
    // event watchers, but this file needs to be renamed now

    /**
     * Shows an image (triggered by clicking on a link, such
     * as a thumbnail or a fashion show link, which contains
     * data about the file's URL).
     */
    var showImageFromData = function(data, $container) {
       if (!data.file) return;
       var $img = $(buildImgFromFileName(data.file));
       var $div = $('<div class="framed-image"></div>')
                    .html($img)
       var $framedImage;
       if ($container) {
        var $outerFrame = build$AbsolutePositionFrame();
        var $innerFrame = $('<div style="position:relative"></div>');
         $framedImage = $outerFrame.append($innerFrame.append($div));
       } else {
         $framedImage = build$FullScreenFrame().html($div);
       }
       showImage($framedImage, $container);
    };

    // Hook up loc-link events
    $('body')
      .on('click', '.loc-link, .fashion-link, .thumbnail-link', function() {
        removeOpenImage(); // get rid of any mouseover images first
        var data = $(this).data();
        var theClickedLinkShouldRedirect = showMapForData(data) || showImageFromData(data);
        return theClickedLinkShouldRedirect || false;
      })
      // Temporarily (?) disable mouseover showing of images
      //.on('mouseenter', '.loc-link, .fashion-link, .thumbnail-link', function(e) {
      //  var $this = $(e.target).on('mouseleave', removeOpenImage);
      //  var data = $(this).data();
      //  showMapForData(data, $this);
      //  showFashionForData(data, $this);
      //});

      
    // *** HELPER FUNCTIONS ***
    // Define two helper functions for building new zone definitions

    // 1) Use this function to find the correct 0,0 point
    window.testZero = window.testZeroZero = function(zoneData) {
      removeOpenImage(); // test functions don't clean up properly
      var locs = [{ x: 0, y: 0 }];
      showMapWithLocs(zoneData, locs);
    };


    // 2) Use this function to generate a grid of alignment of X's
    window.testGrid = function(zoneData) {
      removeOpenImage(); // test functions don't clean up properly
      var $locTd = $('b:contains("Location:")').parent();
      var locs = [];
      for (var x = zoneData.maxX; x >= zoneData.minX; x -= zoneData.interval) {
        for (var y = zoneData.maxY; y >= zoneData.minY; y -= zoneData.interval) {
          locs.push({ x: x, y: y });
        }
      };
      showMapWithLocs(zoneData, locs);
    };

  })();
  } catch (err) {
    console.error(err); // If anything goes wrong, move on but log the error
  }