Yale University Library Map Application Algorithm
// one web file is used, default.aspx
// in .Net this has a "code behind" file where the bulk of things happen.
// There's a lot of localized logic in that file. But in a nutshell the page
// makes a call to the cLocation class shown below like this:
private cLocation cl;
private string _location = "";
private string _callNumber = "";
private string _mapfloor = "";
private string _mapsection = "";
private string _mapid = "";
private string _bibID = "";
private int _manualMode = 0;
protected void Page_Load(object sender, EventArgs e)
{
StringBuilder sb = new StringBuilder();
cl = new cLocation(_location, _callNumber, _mapsection);
Response.ContentType = "text/html";
sb.AppendLine("");
sb.AppendLine(boxBuilder());
}
private string boxBuilder()
{
List coords = cl.getCoordinates();
StringBuilder sb = new StringBuilder();
if (coords.Count > 0)
{
for (int i = 0; i < coords.Count; i++)
{
sb.AppendLine("
");
}
}
return sb.ToString();
}
// the following is the cLocation class called from the default.aspx.cs page
using System;
using System.Collections.Generic;
using System.Data.SqlClient;
using System.Web;
using System.Text.RegularExpressions;
public class cLocation
{
// this stores the location sent from the catalog
private string _location;
// this stores the call number sent from the catalog
public string _callnumber;
// a failsafe URL is provided if we are unable to locate a map to show you, the URL is usually to a page about the location
public string[] _locationError = { "", "" };
// the call numbers are split into an array by white space
public string[] _callSplit;
// MapID is an abbreviated location code used as part of the .GIF name of the map displayed on the web page
public string MapID = "";
// a flag that checks if the book is oversized
public bool isOversized = false;
// a flag that checks if the book is a folio
public bool folio = false;
// We work with two classifications, Yale and LC, the default is LC. The code will try and prove it is Yale instead of the other way around
public string Classification = "LC";
// prefix is the first letters of a call number
public string Prefix = "";
// this refers to the call number range which goes from 0 to 9999
public int Range = 0;
// Both MapSection and _mapSection are numbers that represents a section of a map.
// This is a one to many relationship to coordinates. So a map section may contain several
// coordinates to form an L shaped box on the map
// there are failover mechanisms and checks that require the same value be set twice to see if they match. If there is no match then
// the one more likely to match is used.
public string MapSection = "";
public string _mapSection = "";
// this is the floor of the map, the MapID + mapFloor + ".gif" is how the map is called in the tags on the web page
public string MapFloor = "";
// this list contains all the possible matches found in the database
public List ccodes;
// this is the SQL connection info used
private const string dbReader = "user id=[sql username];"
+ "password=[sql user pwd];"
+ "server=[sql server];"
+ "Trusted_Connection=false;"
+ "database=[sql database];"
+ "connection timeout=10;";
public cLocation(string LOCATION, string CALLNUMBER, string MAPSECTION)
{
if (MAPSECTION.Length > 0)
{
// for debugging, it is possible to force a map to appear by manipulating the URL
_mapSection = MAPSECTION;
MapSection = _mapSection;
// we skip a lot of steps if we know exactly what we want
bypass();
loadCodes();
setURL();
}
else
{
_location = LOCATION;
_callnumber = CALLNUMBER;
_callSplit = _callnumber.Split(new char[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
if (_callnumber.Length > 0)
{
if (_callSplit[0].ToLower() == "folio")
{
string[] temp = new string[_callSplit.Length - 1];
for (int i = 1; i < _callSplit.Length; i++)
temp[i - 1] = _callSplit[i];
_callSplit = new string[temp.Length];
for (int i = 0; i < temp.Length; i++)
_callSplit[i] = temp[i];
}
if (_callSplit[0].ToLower() == "judaica")
{
Classification = "judaica";
string[] temp = new string[_callSplit.Length - 1];
for (int i = 1; i < _callSplit.Length; i++)
temp[i - 1] = _callSplit[i];
_callSplit = new string[temp.Length];
for (int i = 0; i < temp.Length; i++)
_callSplit[i] = temp[i];
}
if (_callSplit[0].ToLower() == "near")
{
Classification = "near";
string[] temp = new string[_callSplit.Length - 2];
for (int i = 2; i < _callSplit.Length; i++)
temp[i - 2] = _callSplit[i];
_callSplit = new string[temp.Length];
for (int i = 0; i < temp.Length; i++)
_callSplit[i] = temp[i];
}
}
ccodes = new List();
// a procedural stack, this is the order in which the operations must fire
// we decided at the start that the classification was LC
// so setClassification() gives the Yale class a chance to override the default
// any number of other classifications would work in the method
setClassification();
// oversized materials are stored separately, so we do a quick check
checkOversize();
getMapID();
loadCodes();
setRange();
setPrefix();
// this happens late because some exceptions must be considered for classification
// for LC the word "folio" appears but for Yale Classification there are other rules
checkFolio();
setURL();
// there are a number of temporary location codes that use "in process" so we have to direct
// users to a general info page
if (_callnumber.Trim().ToLower().IndexOf("in process") >= 0)
{
HttpContext.Current.Response.Redirect(_locationError[1]);
}
}
}
// public methods
public string getImg()
{
// this sets the GIF to call.
return MapID.ToLower()
.Replace("smljud", "sml")
.Replace("smlnear", "sml")
.Replace("smlengin", "sml")
+ "_" + MapFloor + ".gif";
}
public List getCoordinates()
{
// requires mapSection to query [stackMap_coordinates]
List rex = new List();
string mySQL = "/*stackmap get coord*/ select csstop,cssleft,csswidth,cssheight "
+ " from stackmap_coordinates where mapsection = @ms ";
using (SqlConnection conn = new SqlConnection(dbReader))
{
conn.Open();
using (SqlCommand cmd = new SqlCommand(mySQL, conn))
{
SqlParameter p = new SqlParameter();
p.ParameterName = "ms";
p.Value = MapSection;
cmd.Parameters.Add(p);
SqlDataReader reader = cmd.ExecuteReader();
if (reader.HasRows)
{
while (reader.Read())
{
rex.Add(new cCoordinates(
reader.GetInt32(0),
reader.GetInt32(1),
reader.GetInt32(2),
reader.GetInt32(3)));
}
}
}
conn.Close();
conn.Dispose();
}
return rex;
}
// private methods
private void setClassification()
{
try
{
// the Judaica and Near East collections are flagged by location code, we don't use call numbers
if (Classification != "judaica" && Classification != "near")
{
if (_callSplit.Length == 2)
{
// if the call number splits into only 2 segments, it is most likely LC
// so we do a simple check, if it starts with two ucase letters, it is LC
string exp = "[A-Z]{2}";
string temp = _callSplit[0];
if (Regex.IsMatch(temp.Replace("+", ""), exp))
{
Classification = "LC";
}
else
{
Classification = "YALE";
}
}
else
{
// Yale class is an upper case letter, followed by a lower case, followed by any amount of numbers
string exp = "[A-Z]{1}[a-z]{1}\\d*";
string temp = _callSplit[0];
if (Regex.IsMatch(temp.Replace("+", ""), exp))
{
Classification = "YALE";
}
}
}
}
catch { }
}
private void checkOversize()
{
// there are only two things that appear in an oversize call number "+" and "oversize"
// we set the call number to lowercase because "Oversize" appears in all sorts of mized cases
if (_callnumber.ToLower().IndexOf("oversize") >= 0 || _callnumber.IndexOf("+") >= 0)
isOversized = true;
}
private void getMapID()
{
// from the Voyager database we pull all the location codes into
// a table to match them to library locations used in this application
// this query finds the abbreviated code based on the locations in Voyager,
// we refresh the data periodically from Voyager but in general
// these values do not change often
string mySQL = "/*stackmap get map id*/select top 1 mapID, defaultclassification "
+ " from stackmap_locationCode where display_name = @dn ";
string locOverride = "";
using (SqlConnection conn = new SqlConnection(dbReader))
{
conn.Open();
using (SqlCommand cmd = new SqlCommand(mySQL, conn))
{
SqlParameter p = new SqlParameter();
p.ParameterName = "dn";
p.Value = _location;
cmd.Parameters.Add(p);
SqlDataReader reader = cmd.ExecuteReader();
if (reader.HasRows)
{
while (reader.Read())
{
MapID = reader[0].ToString();
locOverride = reader[1].ToString();
}
}
}
conn.Close();
conn.Dispose();
}
// it is possible that even after the call number is parsed, it will be wrong and the database knows the exceptions
// so we let it override the classification here. An example is part of a call number range being in a special
// place in the stacks.
if (locOverride.Trim().Length > 0)
{
Classification = locOverride;
}
// for these three exceptions we know exactly where the materials are.
// it's not ideal, this should be handled as an exception but is so rare that
// adding a few lines to the code is simpler
if (MapID.ToLower() == "smljud")
{
// the Judaica section in SML is all in one place, covering half the floor
MapSection = "115";
MapFloor = "6";
}
else if (MapID.ToLower() == "smlnear")
{
// the Near East section in SML is all in one place, covering half the floor
MapSection = "120";
MapFloor = "6";
}
else if (MapID.ToLower() == "smlengin")
{
// this is a temp move as the engineering library is moving
// map section 149 is always temp storage
// so after engineering moves, a different SML location will be put into the MapID
MapSection = "149";
MapFloor = "1";
}
}
private void bypass()
{
// this method loads a map by the map section in the database, ignoring any location or call numbers sent in the URL
// it will highlight the area requested making it possible to link directly to an area in the stacks
// the purpose is sort of like a kiosk mode or if a web page wants to lead you to a section in the stacks
// it is possible to do the same by sending a fake call number in the URL which would be better in case the stacks
// shift. But this method also comes in real handy for debugging the highlighted coordinates on a map.
string tclass = "";
string mySQL = "select top 1 calltype,oversized,mapid,mapfloor from stackmap where mapsection = @mapsec";
using (SqlConnection conn = new SqlConnection(dbReader))
{
conn.Open();
using (SqlCommand cmd = new SqlCommand(mySQL, conn))
{
SqlParameter p = new SqlParameter();
p.ParameterName = "mapsec";
p.Value = _mapSection;
cmd.Parameters.Add(p);
SqlDataReader reader = cmd.ExecuteReader();
if (reader.HasRows)
{
while (reader.Read())
{
tclass = reader[0].ToString();
isOversized = reader[1].ToString() == "1" ? true : false;
MapID = reader[2].ToString();
MapFloor = reader[3].ToString();
}
}
}
conn.Close();
conn.Dispose();
}
if (tclass.ToLower() == "yale")
{
Classification = "YALE";
}
else if (tclass.ToLower() == "lc")
{
Classification = "LC";
}
else
{
Classification = tclass.ToLower();
}
}
private void setURL()
{
// the URLs that are queried here are for library location specific web pages
// like the Divinity Library or external shelving facility
// these web pages generally provide information about the location,
// hours and links to web pages about the location
string mySQL = "/*stackmap get urls*/ select url, "
+ " (select url from stackMap_URLs where mapid = 'error') as failover "
+ " from stackMap_URLs where mapid = @ms ";
bool runerror = false;
using (SqlConnection conn = new SqlConnection(dbReader))
{
conn.Open();
using (SqlCommand cmd = new SqlCommand(mySQL, conn))
{
SqlParameter p = new SqlParameter();
p.ParameterName = "ms";
p.Value = MapID;
cmd.Parameters.Add(p);
SqlDataReader reader = cmd.ExecuteReader();
if (reader.HasRows)
{
while (reader.Read())
{
_locationError[0] = reader[0].ToString();
_locationError[1] = reader[1].ToString();
}
}
else
{
runerror = true;
}
}
conn.Close();
conn.Dispose();
}
// we store a URL in the database to the general library location page instead of hard coding it
// in case we change the logic for this.
if (runerror)
{
mySQL = "/*stackmap get urls*/ select url from stackMap_URLs where mapid = 'error' ";
using (SqlConnection conn = new SqlConnection(dbReader))
{
conn.Open();
using (SqlCommand cmd = new SqlCommand(mySQL, conn))
{
SqlDataReader reader = cmd.ExecuteReader();
if (reader.HasRows)
{
while (reader.Read())
{
_locationError[1] = reader[0].ToString();
}
}
}
conn.Close();
conn.Dispose();
}
}
}
private void setRange()
{
try
{
// basically we want the first bank of numbers from the call number, if any exist
string temp = _callSplit[0];
MatchCollection matches = Regex.Matches(temp, @"\d+");
string _range = "0";
if (matches.Count > 0)
_range = matches[0].Groups[0].Value;
Range = int.Parse(_range);
}
catch
{
// there's nothing to really catch, if this doesn't work,
// it's not the end of the world
}
}
private void setPrefix()
{
// this is the heavy lifting method that will essentially decide the map section and map floor
// it will loop the codes that are loaded to try and find an exact match
Range = Range <= 0 ? 1 : Range;
string temp = _callSplit[0];
MatchCollection matches = Regex.Matches(temp, "[A-Z|a-z]+");
if (matches.Count > 0)
Prefix = matches[0].Groups[0].Value;
string _prefix = Prefix.ToLower();
if (_prefix.Length > 2)
_prefix = _prefix.Substring(0, 2);
bool foundOne = false;
for (int i = 0; i < ccodes.Count; i++)
{
if (_prefix == ccodes[i].prefix.ToLower())
{
if (Range <= ccodes[i].callEnd && Range >= ccodes[i].callStart)
{
MapSection = ccodes[i].mapSection;
MapFloor = ccodes[i].mapFloor;
foundOne = true;
break;
}
}
}
if (!foundOne)
{
// if an exact match is not found and there is only one match in the database,
// it is probably right. it just means that the database didn't have the proper
// range specified.
if (ccodes.Count == 1)
{
MapSection = ccodes[0].mapSection;
MapFloor = ccodes[0].mapFloor;
}
else
{
for (int i = 0; i < ccodes.Count; i++)
{
if (_prefix[0].ToString() == ccodes[i].prefix.ToLower())
{
if (Range <= ccodes[i].callEnd && Range >= ccodes[i].callStart)
{
MapSection = ccodes[i].mapSection;
MapFloor = ccodes[i].mapFloor;
foundOne = true;
break;
}
}
}
}
}
}
private void checkFolio()
{
if (_callnumber.ToLower().IndexOf("folio") >= 0)
folio = true;
if (Classification == "YALE")
{
// This is rare, this would mean that a location overrode the classification type from LC
// to Yale even though the regex match is LC = two upper case letters. In the Yale
// class two upper case = folio. Luckily, Yale class folios are usually handled
// in the location code and not the call number
if (Regex.IsMatch(Prefix, "[A-Z]{2}"))
{
folio = true;
}
try
{
// alternatively, if position 2 in the call number split of a Yale call number is 00
// it's a folio
if (_callSplit[1].StartsWith("00"))
{
folio = true;
}
}
catch { }
}
}
private void loadCodes()
{
// a very forgiving query retrieves all possible map sections no matter how unlikely they are to match
// this is done to provide failover possibilities. Originally a more rigid query ran but failure rates
// to provide a map location were high forcing additional queries to run.
// an important note about the call prefix, start and end. All letter/number combinations are stored
// even for call numbers that do not exist. This way all fail safe values are stored in SQL rather than
// writing complex exception handling because Qz456.78 does not exist. Instead SQL assumes that it does
// exist but if a map doesn't exist to handle, it fails gracefully. The alternative being some infinite
// loop that can never find it.
string mySQL = "/*stackmap load codes*/ select callprefix,callstart,callend,mapfloor,mapsection from stackmap "
+ " where calltype = @ct and mapid = @mapid and oversized = @ov and left(callprefix,1) = @pre ";
string pre = "";
string temp = _callSplit[0];
for (int i = 0; i < temp.Length; i++)
{
if (Regex.IsMatch(temp[i].ToString(), "\\w"))
{
pre = temp[i].ToString();
break;
}
}
using (SqlConnection conn = new SqlConnection(dbReader))
{
conn.Open();
using (SqlCommand cmd = new SqlCommand(mySQL, conn))
{
SqlParameter p = new SqlParameter();
p.ParameterName = "ct";
p.Value = Classification;
cmd.Parameters.Add(p);
p = new SqlParameter();
p.ParameterName = "mapid";
p.Value = MapID;
cmd.Parameters.Add(p);
p = new SqlParameter();
p.ParameterName = "ov";
p.Value = isOversized;
cmd.Parameters.Add(p);
p = new SqlParameter();
p.ParameterName = "pre";
p.Value = pre;
cmd.Parameters.Add(p);
SqlDataReader reader = cmd.ExecuteReader();
if (reader.HasRows)
{
while (reader.Read())
{
ccodes.Add(new cCode(
reader[0].ToString(),
reader.GetInt32(1),
reader.GetInt32(2),
reader[3].ToString(),
reader[4].ToString()));
}
}
}
conn.Close();
conn.Dispose();
}
}
}
// helper classes
public class cCoordinates
{
// this amounts to the coordinates used to draw a highlghted box over the .GIF image on the web page
public int Top;
public int Left;
public int Width;
public int Height;
// the offsets are literally the number of pixels from left and top that the .GIF image appears on the web page
// if our web templates change, these number have to change as well. The image is placed using an absolute DIV tag
// with top and left specified.
private int xOff = 23;
private int yOff = 121;
public cCoordinates(int TOP, int LEFT, int WIDTH, int HEIGHT)
{
Top = TOP + yOff;
Left = LEFT + xOff;
Width = WIDTH;
Height = HEIGHT;
}
}
public class cCode
{
// these values are all populated from a SQL table and
// used to try and find the best matching location code info
public string prefix;
public int callStart;
public int callEnd;
public string mapFloor;
public string mapSection;
public cCode(string PREFIX, int CALLSTART, int CALLEND, string MAPFLOOR, string MAPSECTION)
{
prefix = PREFIX;
callStart = CALLSTART;
callEnd = CALLEND;
mapFloor = MAPFLOOR;
mapSection = MAPSECTION;
}
}
// SQL table setup
/*
* [stackMap]
* - id (unique)
* - callPrefix (char)
* - callStart (int)
* - callEnd (int)
* - callType (char)
* - oversized (bit)
* - mapID (char)
* - mapFloor (char)
* - mapSection (int)
*
* example row: 120|PL|1|2652|LC|0|SML|2M|40
*
*
* [stackMap_coordinates]
* - id (unique id)
* - mapSection (int)
* - cssTop (int)
* - cssLeft (int)
* - cssWidth (int)
* - cssHeight (int)
*
* example row: 61|40|629|257|107|94
*
*
* [stackMap_locationCode]
* - id (unique)
* - location_code (nvarchar - imported from Voyager table)
* - location_name (nvarchar - imported from Voyager table)
* - display_name (nvarchar - imported from Voyager table)
* - mapID (char)
* - defaultClassification (char)
*
* example row: 1136|smly|smly|SML, Stacks, Yale Classification|sml|YALE
*
*
* [stackMap_URLs]
* - id (unique)
* - MapID (char)
* - URL (nvarchar)
*
* example row: 17|medical|http://library.medicine.yale.edu/about/floor
*/