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 */