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
(One intermediate revision by one user not shown)
Line 136: Line 136:
 
   };
 
   };
  
   var removeOpenMap = function(e) {
+
   var removeOpenImage = function(e) {
 
     // Do nothing if there's no open map
 
     // Do nothing if there's no open map
 
     if (!window.$openImg) return false;
 
     if (!window.$openImg) return false;
Line 149: Line 149:
 
   }
 
   }
  
   var build$FullMap = function(zoneData) {
+
 
     var $frame = build$MapFrame()
+
   var build$FullScreenFrame = function() {
 +
     return build$MapFrame()
 
       .css({
 
       .css({
 
         background: 'rgba(0,0,0,0.8)',
 
         background: 'rgba(0,0,0,0.8)',
Line 160: Line 161:
 
         zIndex: 4 // one higher than #p-search's 3
 
         zIndex: 4 // one higher than #p-search's 3
 
       })
 
       })
       .click(removeOpenMap);
+
       .click(removeOpenImage);
 +
  }
 +
 
 +
  var build$FullMap = function(zoneData) {
 +
    var $frame = build$FullScreenFrame();
 
     if (zoneData) {
 
     if (zoneData) {
       $frame.html(build$LightboxFramedMap(zoneData));
+
       var $img = build$LightboxFramedMap(zoneData);
 +
      $frame.html($img);
 
       $frame.zoneData = zoneData;
 
       $frame.zoneData = zoneData;
 
     }
 
     }
Line 283: Line 289:
 
   */
 
   */
 
   var showMapWithLocs = function(zoneData, locs, $container) {
 
   var showMapWithLocs = function(zoneData, locs, $container) {
     removeOpenMap();
+
     removeOpenImage();
 
     zoneData = addImageUrl(zoneData);
 
     zoneData = addImageUrl(zoneData);
 
     var $map = $container ? build$SmallMap(zoneData) : build$FullMap(zoneData);
 
     var $map = $container ? build$SmallMap(zoneData) : build$FullMap(zoneData);
Line 368: Line 374:
  
 
         var $map = showMapWithLocs(zoneData, locs, $locTd);
 
         var $map = showMapWithLocs(zoneData, locs, $locTd);
         $map.parent().one('mouseleave', removeOpenMap);
+
         $map.parent().one('mouseleave', removeOpenImage);
 
       })
 
       })
 
       // When it's clicked, show the map full-screen/lightbox style
 
       // When it's clicked, show the map full-screen/lightbox style
Line 424: Line 430:
 
   var showFashionForData = function(data) {
 
   var showFashionForData = function(data) {
 
     if (!data.file) return;
 
     if (!data.file) return;
     showImage($('<img src="' + data.file + '" />'));
+
     var $framedImage =
 +
      build$FullScreenFrame()
 +
        .html('<img src="/images/' + data.file + '" />')
 +
    showImage($framedImage);
 
   };
 
   };
  
Line 436: Line 445:
 
     })
 
     })
 
     .on('mouseenter', '.loc-link', function(e) {
 
     .on('mouseenter', '.loc-link', function(e) {
       var $this = $(e.target).on('mouseleave', removeOpenMap);
+
       var $this = $(e.target).on('mouseleave', removeOpenImage);
 
       showMapForData($(this).data(), $this);
 
       showMapForData($(this).data(), $this);
 
     });
 
     });
Line 446: Line 455:
 
   // 1) Use this function to find the correct 0,0 point
 
   // 1) Use this function to find the correct 0,0 point
 
   window.testZero = window.testZeroZero = function(zoneData) {
 
   window.testZero = window.testZeroZero = function(zoneData) {
     removeOpenMap(); // test functions don't clean up properly
+
     removeOpenImage(); // test functions don't clean up properly
 
     var locs = [{ x: 0, y: 0 }];
 
     var locs = [{ x: 0, y: 0 }];
 
     showMapWithLocs(zoneData, locs);
 
     showMapWithLocs(zoneData, locs);
Line 454: Line 463:
 
   // 2) Use this function to generate a grid of alignment of X's
 
   // 2) Use this function to generate a grid of alignment of X's
 
   window.testGrid = function(zoneData) {
 
   window.testGrid = function(zoneData) {
     removeOpenMap(); // test functions don't clean up properly
+
     removeOpenImage(); // test functions don't clean up properly
 
     var $locTd = $('b:contains("Location:")').parent();
 
     var $locTd = $('b:contains("Location:")').parent();
 
     var locs = [];
 
     var locs = [];

Revision as of 00:31, 14 September 2019

try {
(function() {

  // 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')) 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$WrappedMap = function(zoneData) {
    return $('<div class="x-container"></div>')
      .css({ position: 'relative' })
      .append(build$MapImage(zoneData));
  }


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

  var build$MapFrame = function() {
    return $('<div class="map-wrapper"></div>')
            .css({ position: 'absolute' });
  };

  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$MapFrame()
      .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) {
      var $img = build$LightboxFramedMap(zoneData);
      $frame.html($img);
      $frame.zoneData = zoneData;
    }
    return $frame;
  };

  var build$SmallMap = function(zoneData) {
    return build$MapFrame().append(build$WrappedMap(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);
      // 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;
    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);
  };

  // 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 areaColumnIndex = $table.find('th:contains("Area")').index();
        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 $tr = $(tr);
          var $loc = $tr.find('td:eq(' + locationColumnIndex + ')');
          var loc = $loc.text().trim();
          // 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 = $tr.find('td:eq(' + zoneColumnIndex + ')').text().trim();
          var $locLink = $('<a href="' + zone + '" ' +
                           '   data-loc="' + nonLoc + ' ' + loc + '" ' +
                           '   data-zone="' + zone + '"' +
                           '>' + loc + '</a>');
          $loc.html($locLink);
        });
      });
  };

  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:")').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();
    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.'); 
  }

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

    var locs = [parseLoc(data.loc)];
    var zoneData = getZoneData(data.zone, locs);
    if (!zoneData) {
      // If it was a click and not a mouseover
      if (!$target) alertNoData(data.zone);
      return;
    }
    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) {
     if (!data.file) return;
     var $framedImage = 
       build$FullScreenFrame()
         .html('<img src="/images/' + data.file + '" />')
     showImage($framedImage);
  };

  // Hook up loc-link events
  $('body')
    .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', removeOpenImage);
      showMapForData($(this).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
}