MediaWiki:LocMaps.js
From EQArchives
Jump to navigationJump to search
Note: After publishing, you may have to bypass your browser's cache to see the changes.
- Firefox / Safari: Hold Shift while clicking Reload, or press either Ctrl-F5 or Ctrl-R (⌘-R on a Mac)
- Google Chrome: Press Ctrl-Shift-R (⌘-Shift-R on a Mac)
- Internet Explorer / Edge: Hold Ctrl while clicking Refresh, or press Ctrl-F5
- Opera: Press Ctrl-F5.
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;
var match = locText.match(/\(? *([\+\-]?\d+\.?\d*)\s*, *([\+\-]?\d+\.?\d*)\)?/);
return {x: parseFloat(match[2]), y: parseFloat(match[1]) };
}
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 = '';
var bits = locString.split(/([\+\-]?\d+\.?\d*[^0-9\)]*,\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")').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"), th:contains("Loc"))' +
':has(th:contains("Zone"))')
.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:")');
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
}