[ Disclaimer, Create new user --- Wiki markup help, Install P99 ]
MediaWiki:LocMaps.js
From Project 1999 Wiki
Note: After saving, 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: Hold Ctrl while clicking Refresh, or press Ctrl-F5
- Opera: Clear the cache in Tools → Preferences
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 ); }; var getZoneLevelData = function(levels, text) { // Check for part "level" aliases (eg. "1st floor" vs. "Level One") text = text.toLowerCase(); if (containsAnyNumber(text, 0, 'th') || containsAny(text, 'basement', 'underground', 'tunnel')) 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]; 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) { 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) return getZoneLevelData(zone.levels, nonLocParts); // 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 buildX = function(left, top, sizeInEm) { sizeInEm = sizeInEm || 2.5; 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*), *([\+\-]?\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*\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 * 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'); $map.find('img').load(function() { addXs($xContainer, zoneData, locs); }); 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:")').parent().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 $locTd = $('b:contains("Location:"), b:contains("Loc:")').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 $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); }); // $locTd.find('b').html($('<span>Location </span>').append($link).append('<span>:</span>')); $locTd .find('b') .html($('<span>Location </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 alertNoData = function(zone) { alert('We\'re sorry, but ' + zone + ' has not been loc mapped yet ... ' + 'please see "/Loc_Maps" for more information. You\'ll now be taken ' + 'to the zone\'s page instead, where you can use its map as a guide.'); } 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; alertNoData(data.zone); return true; } 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 var showFashionForData = 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', function() { removeOpenImage(); // get rid of any mouseover images first var data = $(this).data(); var allowLinkFollowing = showMapForData(data) || showFashionForData(data); return allowLinkFollowing || false; }) // Temporarily (?) disable mouseover showing of images //.on('mouseenter', '.loc-link, .fashion-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 }