view www/search.js @ 2484:71e565a46274

Change WebSocket port from TCP 444 to 5222 and also fix the WS connect failure / error message since we have supported IPv6 for a while.
author Matti Hamalainen <ccr@tnsp.org>
date Sat, 18 Feb 2023 17:52:14 +0200
parents 4541c68e1ebf
children 81a0f6e6256c
line wrap: on
line source

//
// Frontend map/location search JavaScript functionality
// Written by Matti 'ccr' Hamalainen <ccr@tnsp.org>
// (C) Copyright 2017-2019 Tecnic Software productions (TNSP)
//

var fieldPattern, chosenPattern = 0;
var msgLog, mapWS, tmpWS, locWS, mapList = [];
var mapItemToggles = new Array();

var mapExamples =
[
"^^HHHHHHHH+HHHHHH^^^H\n"
,
"~~~~~~~~~~~~hh^hhhhh\n"+
"~~~~~~~~~~~~~hhh~~~~\n"+
"~~~~~~~~~~~~~h~~~~~~\n"+
"~~~~~~~~~~~~~~~~~~~~\n"+
"~~~~~~~~~~~~~~~~~~~~\n"+
"~~h~~~~~~~~~~~~~~~~~\n"+
"~h^^f~~~~*~~~~~~~~~~\n"+
"~~h^fff~~~~~~~~~~~~~\n"+
"~~~ff^f~~~~~~~~~~~~~\n"+
"~~~~fff~~~~~~h~~~~~~\n"+
"~~~~~~~~~~~h~h~~~~~~\n"+
"~~~~~~~~~~h^^^hh~~~~\n"+
"~~~~~~~~~~h^^^^^hh~~\n"
,

" lljjjjjjjjhhhhjjjjjjjjjjj \n"+
" llljjjjjjjjhhhhjjjjjjjjjj \n"+
" lllljjjjjjjhhhhjjjjjjjjjj \n"+
" jjllljjjjjjjjhhhhjjjjjjjj \n"+
"jjjllllljjjjj*jjjhjjjjjjjjj\n"+
" jllllllljjjjjjjjjjjjjjjjj \n"+
" jlllllllljjjjjjjjjjjjjjjj \n"+
" jlllllllljjjjjjjjjjjjjjjj \n"+
" jjlljllllljjjjjjjjjjjjjjj \n"
,
"             S             \n"+
"        SSSSSSSSSS~        \n"+
"       SSSSSSSSSSS~~       \n"+
"     SSSSsyssSSSSSSS~~     \n"+
"    SSSSsyyssssSSSSS~~~    \n"+
"   SSSSssssysssSSSSS~~~~   \n"+
"   SSSSysshsyyssSSSS~~~~   \n"+
"  SSSSSysyhhssssSSSSS~~~~  \n"+
" SSSSSSysshhsssssSSSSS~~~~ \n"
,
"....|...|\n"+
"----*---+\n"+
"yy.y....|\n"
];



function mapCE(obname, obid)
{
  var mob = document.createElement(obname);
  if (obid)
    mob.id = obid;
  return mob;
}


function mapClearChildren(obnode)
{
  if (obnode == null || typeof(obnode) == 'undefined')
    return;

  while (obnode.firstChild)
    obnode.removeChild(obnode.firstChild);
}


function mapAddEventOb(obname, evobj, evtype, evcallback)
{
  if (evobj == null || typeof(evobj) == 'undefined')
  {
    console.log("Event object '"+ obname +"' == null.");
    return;
  }

  if (evobj.addEventListener)
    evobj.addEventListener(evtype, evcallback, false);
  else
  if (evobj.attachEvent)
    evobj.attachEvent("on"+evtype, evcallback);
  else
    evobj["on"+evtype] = evcallback;
}


function mapAddEvent(obname, evtype, evcallback)
{
  mapAddEventOb(obname, document.getElementById(obname), evtype, evcallback);
}


function mapToggleView(id)
{
  var elem = document.getElementById(id);
  if (elem)
  {
    var val = true;
    if (id in mapItemToggles)
      val = mapItemToggles[id];

    mapItemToggles[id] = !val;

    elem.style.display = val ? "block" : "none";
  }
}


function mapCapitalize(str)
{
  return str.substr(0, 1).toUpperCase() + str.substr(1);
}


function mapResult(msg)
{
  var elem = document.getElementById("results");
  if (elem)
    elem.innerHTML = msg;
}


function mapValidateJSON(edata, elen1, elen2)
{
  var results;
  try { results = JSON.parse(edata); }
  catch (err) { return "Failed to parse JSON: "+ err.message; }

  if (results && Array.isArray(results))
  {
    for (var i = 0; i < results.length; i++)
    {
      var res = results[i];
      if (!Array.isArray(res))
        return "Invalid data.";

      if (i == 0 && res.length != elen1)
        return "Invalid data. First element mismatch.";
      else
      if (i > 0 && res.length != elen2)
        return "Invalid data. Element mismatch.";
    }
    return results;
  }
  else
    return "Could not parse result dataset."
}


function mapGetSelectedMaps()
{
  // Check which maps are enabled, if any
  var searchList = [];
  for (var i = 0; i < mapList.length; i++)
  {
    var res = mapList[i];
    var elem = document.getElementById("map_"+ res[0]);
    if (elem && elem.checked)
      searchList.push(res[0]);
  }
  return searchList;
}


function mapUpdateMapCount()
{
  var elem = document.getElementById("mapInfo");
  var l = mapGetSelectedMaps();
  elem.innerHTML = (l.length == 0) ?
    "No maps selected!" :
    "<b>"+ l.length +"</b> map"+ (l.length > 1 ? "s" : "") +" selected.";
}


function mapSetupWebSocket()
{
  if ("WebSocket" in window)
  {
    var mapPort = "5222";
    var tmpWS = new WebSocket("wss://tnsp.org:"+ mapPort);
    if (!tmpWS)
    {
      mapResult("WebSocket error: Could not create WebSocket.");
      return null;
    }

    tmpWS.onerror = function(mev)
    {
      mapResult("<b>WebSocket error occured!</b>"+
      "<p>If this problem persists for more than few hours AND this search HAS worked for you before, "+
      "you should check if your Internet provider and firewall allow connections to TCP port "+ mapPort +".</p>"
      );
      console.error("WebSocket error occured: ", mev);
    };

    return tmpWS;
  }
  else
  {
    mapResult("Your browser does not support WebSockets.");
    return null;
  }
}


function mapHandleError(str)
{
  if (str.substr(0, 6) == "ERROR:")
  {
    mapResult("ERROR! "+ str.substr(6).trim());
    return false;
  }
  else
    return true;
}


function mapGetData()
{
  var dataWS = mapSetupWebSocket();
  if (!dataWS)
    return;

  var mlobj = document.getElementById("mapList");
  if (mlobj)
    mlobj.innerHTML = "Contacting server ...";

  dataWS.onopen = function()
  {
    if (mlobj)
      mlobj.innerHTML = "Loading maplist ...";

    dataWS.send("GETMAPS");
  };

  dataWS.onmessage = function(evt)
  {
    if (mapHandleError(evt.data))
    if (evt.data.substr(0, 5) == "MAPS:" && evt.data.length > 8)
    {
      var results = mapValidateJSON(evt.data.substr(5), 3, 3);
      if (Array.isArray(results) && results.length > 0)
      {
        mapList = results;

        // Create list of map selection buttons
        var mobj = document.getElementById("mapList");
        mapClearChildren(mobj);

        for (var i = 0; i < results.length; i++)
        {
          var res = results[i];
          var id = "map_"+ res[0];

          // Checkbox input
          var mbut = mapCE("input", id);
          mbut.type = "checkbox";
          mbut.checked = true;
          mbut.className = "map";

          mapAddEventOb(id, mbut, "change", mapUpdateMapCount);
          mobj.appendChild(mbut);

          // Label for button
          var mlabel = mapCE("label");
          mlabel.htmlFor = id;
          mlabel.textContent = mapCapitalize(res[0]);
          mobj.appendChild(mlabel);
        }

        mapUpdateMapCount();
      }
      else
        mapResult("ERROR: "+ results);
    }
    else
      mapResult("ERROR: Unknown reply from server.");

    dataWS.close();
  };
}


function mapGetLocType(flags)
{
  switch (flags & 0xfffc)
  {
    case 0x0004: return "PCITY";
    case 0x0008: return "CITY";
    case 0x0010: return "SHRINE";
    case 0x0020: return "GUILD";
    case 0x0040: return "SS";
    case 0x0080: return "MONSTER";
    case 0x0100: return "TRAINER";
    case 0x0200: return "FORT";
    default: return null;
  }
}


function mapGetLocPrefix(flags)
{
  var str = mapGetLocType(flags);
  return str != null ? "["+ str + "] " : "";
}


function mapGetGMapLink(gx, gy)
{
  return "[ <a class=\"glob\" target=\"_blank\" "+
    "href=\"http://jeskko.pupunen.net/gmap2/?x="+ gx +"&y="+
    gy +"&zoom=10\">"+ gx +", "+ gy +"</a> ]";
}


function mapGetNearby(mx, my, mid)
{
  var qtmpWS = mapSetupWebSocket();
  if (!qtmpWS)
    return;

  qtmpWS.onopen = function(evt)
  {
    qtmpWS.send("LOCNEAR:"+ mx +":"+ my +":"+ 200 +":2:");
  };

  qtmpWS.onmessage = function(evt)
  {
    if (mapHandleError(evt.data))
    if (evt.data.substr(0, 7) == "RESULT:" && evt.data.length >= 9)
    {
      var results = mapValidateJSON(evt.data.substr(7), 2, 9);

      if (Array.isArray(results) && results.length > 0 && mid)
      {
        var str = "";

        // Format results, if any
        for (var i = 1; i < results.length; i++)
        {
          var res = results[i];
          var flags = res[5];
          var names = res[8];

          str += "<div class=\"nearby\" title=\""+ names.join(" | ") +"\">"+
            "<a target=\"_blank\" href=\""+ res[0] +".html#loc"+
            res[1] +"_"+ res[2] +"\">"+
            mapGetLocPrefix(flags) + names[0] +"</a>" +
            " "+ mapGetGMapLink(res[3], res[4]) +
            " dist "+ res[7] +
            "</div>";
        }

        mid.innerHTML += str;
      }
    }

    qtmpWS.close();
  };
}


function mapDoMapSearch()
{
  // Check the search pattern for some sanity before
  // submitting to the server .. though we do checks there also.
  var tmp = fieldPattern.value.trim();
  if (tmp == "")
  {
    mapResult("Nothing to search for.");
    return;
  }

  if (tmp.length > 25*25)
  {
    mapResult("Search pattern too large!");
    return;
  }

  var searchList = mapGetSelectedMaps();
  if (searchList.length == 0 && mapList.length > 0)
  {
    mapResult("No maps selected!");
    return;
  }

  // Are we running an old query?
  if (mapWS)
  {
    mapResult("Old query not finished.");
    return;
  }

  // Open a WebSocket connection ..
  mapWS = mapSetupWebSocket();
  if (!mapWS)
    return;

  btnMapSearch.disabled = true;

  mapWS.onopen = function()
  {
    // Web Socket is connected, send data using send()
    mapWS.send("MAPSEARCH:"+ -1 +":"+ searchList.join(":") +"\n" + fieldPattern.value);
  };

  mapWS.onclose = function()
  {
    mapWS = null;
    btnMapSearch.disabled = false;
  };

  // Register events
  mapWS.onmessage = function(evt)
  {
    if (mapHandleError(evt.data))
    if (evt.data.substr(0, 7) == "RESULT:" && evt.data.length >= 9)
    {
      var results = mapValidateJSON(evt.data.substr(7), 7, 5);

      if (Array.isArray(results) && results.length > 0)
      {
        var glb = results[0];
        var str =
          "<div class=\"resultHead\">Search pattern size <b>"+ glb[5] +" x "+ glb[6] +"</b>"+
          (glb[2] ? " (Centered at <b>"+ glb[3] +", "+ glb[4] +"</b>)" : " (Not centered)") +
          "</div><div class=\"resultHead\"><b>"+ glb[0] +"</b> match"+
          ((glb[0] > 1 || glb[0] == 0) ? "es" : "") +" found, match limit is "+ glb[1] +".</div>";

        // First element is the global info
        if (results.length <= 1)
          str += "<div class=\"resultHead\">No matches found.</div>";

        var mobj = document.getElementById("results");
        mobj.innerHTML = str;

        // Format results, if any
        for (var i = 1; i < results.length; i++)
        {
          var res = results[i];
          var mid = mapCE("div");
          mid.className = "resultData";
          mid.innerHTML = "<span class=\"resultData\">"+ res[1] +", "+ res[2] +" at "+
            mapCapitalize(res[0]) +" "+
            mapGetGMapLink(res[3], res[4]) +
            "</span>";

          mobj.appendChild(mid);
          mapGetNearby(res[3], res[4], mid);
        }
      }
      else
        mapResult("ERROR: "+ results);
    }
    else
      mapResult("ERROR: Unknown reply from server.");

    mapWS.close();
  };
}


function mapDoLocSearch()
{
  locPID = -1;

  // Check the search pattern for some sanity before
  // submitting to the server .. though we do checks there also.
  var nearby = false;
  var tmp = fieldLocPattern.value.trim();
  if (tmp == "")
  {
    mapResult("Nothing to search for.");
    return;
  }

  if (tmp.length > 30)
  {
    mapResult("Search pattern too large!");
    return;
  }

  if (tmp.substr(0, 1) == "@")
  {
    nearby = true;
    tmp = tmp.substr(1).trim().replace(",", ":") + ":";
  }
  else
  {
    // Allow exact matches
    if (tmp.substr(0, 1) == "^")
      tmp = tmp.substr(1);
    else
      tmp = "*"+ tmp;

    if (tmp.substr(-1) == "$")
      tmp = tmp.substr(-1);
    else
      tmp = tmp +"*";
  }

  // Are we running an old query?
  if (locWS)
    return;

  // Open a WebSocket connection ..
  locWS = mapSetupWebSocket();
  if (!locWS)
    return;

  locWS.onopen = function()
  {
    // Web Socket is connected, send data using send()
    locWS.send((nearby ? "LOCNEAR:" : "LOCSEARCH:") + tmp);
  };

  locWS.onclose = function()
  {
    locWS = null;
  };

  // Register events
  locWS.onmessage = function(evt)
  {
    if (mapHandleError(evt.data))
    if (evt.data.substr(0, 7) == "RESULT:" && evt.data.length >= 9)
    {
      var results = mapValidateJSON(evt.data.substr(7), 2, 9);

      if (Array.isArray(results) && results.length > 0)
      {
        var glb = results[0];
        var str =
          "<div class=\"resultHead\"><b>"+ glb[0] +"</b> match"+
          ((glb[0] > 1 || glb[0] == 0) ? "es" : "") +" found, match limit is "+ glb[1] +".</div>";

        if (results.length <= 1)
          str += "<div class=\"resultHead\">No matches found.</div>";

        // Format results, if any
        for (var i = 1; i < results.length; i++)
        {
          var res = results[i];
          var flags = res[5];
          var nname = res[6];
          var names = res[8];

          str += "<div class=\"resultData\" title=\""+ names.join(" | ") +"\">"+
            "<a target=\"_blank\" href=\""+ res[0] +".html#loc"+
            res[1] +"_"+ res[2] +"\">"+
            mapGetLocPrefix(flags) + names[0] +"</a>" +
            (nname > 0 ? " ["+ names[nname] +"]" : "") +
            " at "+ res[1] +", "+ res[2] +" on "+ mapCapitalize(res[0]) +
            " "+ mapGetGMapLink(res[3], res[4]) +
            (nearby ? " dist "+ res[7] : "")+
            "</div>";
        }

        mapResult(str);
      }
      else
        mapResult("ERROR: "+ results);
    }
    else
      mapResult("ERROR: Unknown reply from server.");

    locWS.close();
  };
}


function mapInitSearch()
{
  // Check for disabled Javascript
  var verr = document.getElementById("noscript");
  if (verr)
    verr.parentNode.removeChild(verr);

  var mlhelp1 = document.getElementById("help1");
  if (mlhelp1)
    mlhelp1.style.display = "none";

  var mlhelp2 = document.getElementById("help2");
  if (mlhelp2)
    mlhelp2.style.display = "none";

  if (!("WebSocket" in window))
  {
    mapResult("Your browser does not support WebSockets!");
    return;
  }

  fieldPattern = document.getElementById("mapPattern");
  btnMapSearch = document.getElementById("btnMapSearch");

  // Show example search pattern
  chosenPattern = Math.floor(Math.random() * mapExamples.length);
  mapAddEvent("btnExample", "click",
  function ()
  {
    mapResult("Example search pattern <b>#"+ (chosenPattern + 1) +
      "/"+ mapExamples.length +"</b> selected. Try clicking 'Search' now.");

    fieldPattern.value = mapExamples[chosenPattern];
    chosenPattern = (chosenPattern + 1) % mapExamples.length;
  });

  // Map search button
  mapAddEventOb("btnMapSearch", btnMapSearch, "click", mapDoMapSearch);

  // Clear search button
  mapAddEvent("btnClear", "click",
  function ()
  {
    fieldPattern.value = "";
    mapResult("Cleared search pattern and results.");
  });

  mapGetData();

  // Reset or clear map list button
  mapAddEvent("btnMaps", "click",
  function ()
  {
    var same = true, first = true;
    // Get current state of map list
    for (var i = 0; i < mapList.length; i++)
    {
      var elem = document.getElementById("map_"+ mapList[i][0]);
      if (first)
        same = elem.checked;
      else
      if (elem.checked != same)
      {
        same = false;
        break;
      }
      first = false;
    }

    // Invert it
    same = !same;

    // Set it! Bop it!
    for (var i = 0; i < mapList.length; i++)
    {
      var elem = document.getElementById("map_"+ mapList[i][0]);
      elem.checked = same;
    }

    mapUpdateMapCount();
  });

  // Listener for location name search
  fieldLocPattern = document.getElementById("locPattern");
  if (fieldLocPattern)
  {
    locPID = -1;
    mapAddEventOb("locPattern", fieldLocPattern, "input",
    function ()
    {
      if (locPID != -1)
        clearTimeout(locPID);

      locPID = setTimeout(mapDoLocSearch, 500);
    });
  }
}