').attr({class:'portalDetails'}).html(portalDetailedDescription),
        $('
![]()
').attr({class:'hide', src:img})
      ),
      modDetails,
      miscDetails,
      resoDetails,
      statusDetails,
      '
' + linkDetails.join('') + '
'
    );
  // only run the hooks when we have a portalDetails object - most plugins rely on the extended data
  // TODO? another hook to call always, for any plugins that can work with less data?
  if (details) {
    runHooks('portalDetailsUpdated', {guid: guid, portal: portal, portalDetails: details, portalData: data});
  }
}
window.getPortalMiscDetails = function(guid,d) {
  var randDetails;
  if (d) {
    // collect some random data that’s not worth to put in an own method
    var links = {incoming: 0, outgoing: 0};
    $.each(d.portalV2.linkedEdges||[], function(ind, link) {
      links[link.isOrigin ? 'outgoing' : 'incoming']++;
    });
    function linkExpl(t) { return '
'+t+''; }
    var linksText = [linkExpl('links'), linkExpl(' ↳ ' + links.incoming+'  •  '+links.outgoing+' ↴')];
    var player = d.captured && d.captured.capturingPlayerId
      ? '
' + d.captured.capturingPlayerId + ''
      : null;
    var playerText = player ? ['owner', player] : null;
    var time = d.captured
      ? '
'
        +  unixTimeToString(d.captured.capturedTime) + ''
      : null;
    var sinceText  = time ? ['since', time] : null;
    var fieldCount = getPortalFieldsCount(guid);
    var linkedFields = ['fields', fieldCount];
    // collect and html-ify random data
    var randDetailsData = [];
    if (playerText && sinceText) {
      randDetailsData.push (playerText, sinceText);
    }
    randDetailsData.push (
      getRangeText(d), getEnergyText(d),
      linksText, getAvgResoDistText(d),
      linkedFields, getAttackApGainText(d,fieldCount),
      getHackDetailsText(d), getMitigationText(d)
    );
    // artifact details
    //niantic hard-code the fact it's just jarvis shards/targets - so until more examples exist, we'll do the same
    //(at some future point we can iterate through all the artifact types and add rows as needed)
    var jarvisArtifact = artifact.getPortalData (guid, 'jarvis');
    if (jarvisArtifact) {
      // the genFourColumnTable function below doesn't handle cases where one column is null and the other isn't - so default to *something* in both columns
      var target = ['',''], shards = ['shards','(none)'];
      if (jarvisArtifact.target) {
        target = ['target', '
'+(jarvisArtifact.target==TEAM_RES?'Resistance':'Enlightened')+''];
      }
      if (jarvisArtifact.fragments) {
        shards = [jarvisArtifact.fragments.length>1?'shards':'shard', '#'+jarvisArtifact.fragments.join(', #')];
      }
      randDetailsData.push (target, shards);
    }
    randDetails = '
' + genFourColumnTable(randDetailsData) + '
';
  }
  return randDetails;
}
// draws link-range and hack-range circles around the portal with the
// given details. Clear them if parameter 'd' is null.
window.setPortalIndicators = function(p) {
  if(portalRangeIndicator) map.removeLayer(portalRangeIndicator);
  portalRangeIndicator = null;
  if(portalAccessIndicator) map.removeLayer(portalAccessIndicator);
  portalAccessIndicator = null;
  // if we have a portal...
  if(p) {
    var coord = p.getLatLng();
    // range is only known for sure if we have portal details
    // TODO? render a min range guess until details are loaded..?
    var d = portalDetail.get(p.options.guid);
    if (d) {
      var range = getPortalRange(d);
      portalRangeIndicator = (range.range > 0
          ? L.geodesicCircle(coord, range.range, {
              fill: false,
              color: RANGE_INDICATOR_COLOR,
              weight: 3,
              dashArray: range.isLinkable ? undefined : "10,10",
              clickable: false })
          : L.circle(coord, range.range, { fill: false, stroke: false, clickable: false })
        ).addTo(map);
    }
    portalAccessIndicator = L.circle(coord, HACK_RANGE,
      { fill: false, color: ACCESS_INDICATOR_COLOR, weight: 2, clickable: false }
    ).addTo(map);
  }
}
// highlights portal with given GUID. Automatically clears highlights
// on old selection. Returns false if the selected portal changed.
// Returns true if it's still the same portal that just needs an
// update.
window.selectPortal = function(guid) {
  var update = selectedPortal === guid;
  var oldPortalGuid = selectedPortal;
  selectedPortal = guid;
  var oldPortal = portals[oldPortalGuid];
  var newPortal = portals[guid];
  // Restore style of unselected portal
  if(!update && oldPortal) setMarkerStyle(oldPortal,false);
  // Change style of selected portal
  if(newPortal) {
    setMarkerStyle(newPortal, true);
    if (map.hasLayer(newPortal)) {
      newPortal.bringToFront();
    }
  }
  setPortalIndicators(newPortal);
  runHooks('portalSelected', {selectedPortalGuid: guid, unselectedPortalGuid: oldPortalGuid});
  return update;
}