Adding a map to Webdrawer

My out-of-the-box webdrawer interface lacks a map for the results!  Here's what it looks like right now....

2017-09-25_10-20-37.png

Let's fix it!


If I open the results list partial class file in the "/Views/Shared" directory, I can partition the page into two columns.

 
<table id="container" margin="5">
    <thead>
        <tr>
            <td width="50%"><span style="margin-left:10px">Results List</span></td>
            <td width="50%">Results Map</td>
        </tr>
    </thead>
    <tbody>
    <td id="listColumn">
            <!-- Search Results go here -->
    </td>
    <td id="mapColumn"><div id="mapDiv"></div></td>
    </tbody>
</table>

Next we import the google maps API

 
<script type="text/javascript" src="https://maps.googleapis.com/maps/api/js?key=123">
</script>

But I also need to add some code that results in the map being loaded...

 
		<script type="text/javascript">
			var map;
			var markers = [];
			var mapmarkers = [];
			function initialize() {
				var mapOptions = {
					center: new google.maps.LatLng(27.897349,-82.155762),
					zoom: 7,
					disableDefaultUI: true,
					zoomControl: true,
					panControl: true,
					mapTypeControl: true,
					scaleControl: true,
					streetViewControl: true,
					rotateControl: true,
					overviewMapControl: true
				};
 
				map = new google.maps.Map(document.getElementById("mapDiv"), mapOptions);
				addMarkers(map);
			}
            google.maps.event.addDomListener(window, "load", initialize);
		</script>

This gives me my desired user interface!

 
2017-09-25_10-47-07.png

Next I need to have Webdrawer "addMarker" for each record in the results set.  I decided to do this at the very bottom of the results list file, but only if there are records.  And then only for those having a GPS location.

 
if (Model.Results.Count > 0)
{
	<script type="text/javascript">
	@foreach (TrimObject record in Model.Results)
	{
		var gpsloc = record.GetPropertyOrFieldString("RecordGpsLocation");
		if ( !String.IsNullOrWhiteSpace(gpsloc) )
		{
			@Html.Raw("addMarker('" + record.GetPropertyOrFieldString("RecordNumber"+ "','" + gpsloc + "');");
		}
	}
	</script>
}

So now I've got the page calling "addMarker" for each record in the search results.  I need to store each one in memory by pushing it into the array of markers.  I also need to define the addMarkers method called by the initialize function during page load.  In that method I iterate each of the array elements, create a marker on the map, and store that marker reference for later manipulation.

 
<script>
    function addMarker(rnum, ltlg) {
        var marker = { "rnum": rnum, "ltlg": ltlg };
        markers.push(marker);
    }
    function addMarkers(map) {
        for (i = 0; i < markers.length; i++) {
            var marker = markers[i];
 
            if (marker.ltlg.indexOf('POINT('>= 0) {
                var ltlg = marker.ltlg.replace('POINT(''').replace(')''').split(' ');
                var mapmarker = new google.maps.Marker({
                    position: new google.maps.LatLng(ltlg[1], ltlg[0]),
                    map: map,
                    title: marker.rnum
                });
                mapmarkers.push(mapmarker);
            }
        }
    }
</script>

Now if I refresh my results window I can see records plotted on the map.  Success!

 
2017-09-25_11-37-30.png

Whoops.... it's becoming clear that my data import is messed up.  It's including locations beyond my desired regional boundary.  I should only be importing Florida facilities!  That issue will be tabled for now.

Extracting Webdrawer User Queries

Content Manager does now capture user queries into the workgroup server logs, but it doesn't give you any information about Webdrawer.  Luckily IIS comes to the rescue via the site logs automatically enabled on the server.  Every time a user requests a resource from the IIS site, an entry is added to the daily log file contained inside W3SVC directory in the "c:\inetpub\logs\LogFiles" folder (or wherever your techie has redirected those logs to).

These logs can be parsed by a host of applications.  A quick google search yields a treasure trove of free applications that can analyze these logs and expose important information.  What they lack though is the unique signature of record queries.  

We can use Powershell to target just the record queries though!

2017-09-13_8-28-07.png

After running the Powershell script, I can see that there have been 51 searches for "All" records and 17 searches for "Registered On=This Year".  Right now this PowerShell script is simply writing to my screen, but in a later post we'll do much much more!  For instance, I'll write the results to a file within my Webdrawer instance.... so that the main landing page provides a direct link to the top 5 searches people perform (obviating the need for users to always use the awkward search interface).

My PowerShell script is as follows:

$queries = [ordered]@{}
$sourceDir = 'C:\inetpub\logs\LogFiles\W3SVC1'
$logs = Get-ChildItem $sourceDir -Filter *.log
foreach ( $log in $logs ) 
{
    $content = get-content "$($sourceDir)\$($log)" |%{$_ -replace '#Fields: ', ''} |?{$_ -notmatch '^#'} | ConvertFrom-Csv -Delimiter ' '
    foreach ( $entry in $content ) 
    {
        if ( $entry.'cs-uri-stem' -like '*/Record' ) 
        {
            if ( $queries.Contains($entry.'cs-uri-query') ) 
            {
                $queries[$entry.'cs-uri-query']++
            } else {
                $queries.Add($entry.'cs-uri-query',1)
            }
        }
    }
}
$queries | Format-Table

Creating the Word List for Webdrawer

In my previous post I showed what a "Word Explorer" could look like within Webdrawer.  It took about an hour and a half to create, but I'll save you that time and explain it here in detail.  If you do end up using this concept please let me know!  

Outline of required changes:

  1. Add new route in the configuration file
  2. Create the WDWordList template
  3. Update the Webdrawer Layout
  4. Update the search results page

It's really that simple! :)

First things first, make sure you're using a non-production instance of Webdrawer and that it's fully functional.  I did all of this within a brand new instance (so there's no chance of messing up something I might need later).  That also means I have very little in terms of content, but that doesn't matter to me.


Add new route in configuration file

Navigate to the installation path of Webdrawer and edit the hptrim.config file in the root.

2017-09-13_8-28-07.png

Within the configuration file, locate and duplicate the existing WDRecordList route.  Then update the new route so that it has a name of "Words", uses the template "WDWordList", includes only the RecordTitle & RecordNotes properties, and has a pageSize of 100,000.  These changes means a new "/Words" path is available to users, which will expose the Title & Notes of up to 100,000 records in a given record search.

2017-09-13_8-28-07.png

Save your changes and you're done with this step.


Create the WDWordList template

Navigate into the Views sub-folder and create a new file named "WDWordList.cshtml".  This new file will generate the Word List to be displayed to the right of the search results.  We're going to include it within an iframe on the search results page.  

Here is the content of my WDWordList file:

@using HP.HPTRIM.Service
@using Resources;
@using HP.HPTRIM.Web.Configuration;
@using HP.HPTRIM.ServiceModel;
@using System.Linq;
@using System.Collections.Generic;
@inherits TrimViewPage<HP.HPTRIM.ServiceModel.RecordsResponse>
 
 
@if (Model != null && Model.Results != null && Model.Results.Count > 0)
{
	var wordCounts = new Dictionary<string, long>();
	foreach (TrimObject record in Model.Results)
	{
		Record docRecord = record as Record;
		foreach ( string word in docRecord.Title.Value.Split(' ').ToList() ) 
		{
			if ( word.Length > 1 ) {
			if ( wordCounts.ContainsKey(word) ) {
				wordCounts[word]++;
			} else {
				wordCounts.Add(word, 1);
			}
			}
		}
	}
	
	<script language="JavaScript">
	<!--
	function getParameterByName(name, url) {
		if (!url) url = window.top.location.href;
		name = name.replace(/[\[\]]/g, "\\$&");
		var regex = new RegExp("[?&]" + name + "(=([^&#]*)|&|#|$)"),
        results = regex.exec(url);
		if (!results) return null;
		if (!results[2]) return '';
		return decodeURIComponent(results[2].replace(/\+/g, " "));
	}
	function ExploreWord(word){
		var searchQuery = getParameterByName("q", window.top.location.href);
		var newUrl = window.top.location.href.replace(searchQuery, "(" + searchQuery + ") AND title:" + word);
		window.top.location.href = newUrl;
	}
	//-->
	</script>
	<table align=center>
	<tr>
		<th>Word</th>
		<th>Count</th>
	</tr>
	
	@{
		var ordered = wordCounts.OrderByDescending(x=>x.Value);
		foreach ( var item in ordered ) {
			<tr>
				<td><a href="javascript:ExploreWord('@item.Key')">@item.Key</a></td>
				<td>@item.Value</td>
			</tr>
		}
	}
	
	</table>
}

Update the Webdrawer Layout

In the "Views\Shared" folder there is a file named "_Layout.cshtml".  This file defines the page layout/structure to be used by all files within Webdrawer.  The problem we have is that the WordList we're going to display is in an iframe and will show the banner & menus, which we don't want.  So the task here is to hide the banner & menus, but only for this new page.  

I accomplished this by placing an if statement around the section of the file that outputs the page banner and menu.  The condition being checked is where I've passed "nb=true" (no banner=true) in the query string of the page.  Later when we embed the word list into the search results page, we'll make sure we're passing this in the query string.

    <body class="@(hideMenu() ? "no-image" : null)">
      	@if ( HttpContext.Current.Request.QueryString["nb"] == null )
		{
			<div id="maincontainer">
			
					<div id="topsection">
						<div class="blue-line">&nbsp;</div>
						<div class="innertube">
							<img id="banner-img-large" src="~/images/top-banner-logo.png" />
							<img id="banner-img-small" src="~/images/top-banner-logo-title-SM.png" />
						</div>
						@if (showLogout() || WebDrawerConfiguration.Instance.UISettings.ShowUserLink)
						{
							<div id="logout-btn" class="dropdown pull-right">
								<a class="btn dropdown-toggle" data-toggle="dropdown" href="#">@this.TrimHelper.CurrentUser.FormattedName <span class="caret"></span></a>
								<ul class="dropdown-menu" role="menu" aria-labelledby="dLabel">
									@if (showLogout())
									{
										<li>
											<a href="~/auth/adlogout">Logout</a>
										</li>
									}
									<li>
										<a href="~/Location/Me">Profile</a>
									</li>
								</ul>
							</div>
						}
 
					</div>
				
				<div id="contentwrapper" class="@(hideMenu() ? "no-menu" : null)">
					<div id="contentcolumn" class="@(hideMenu() ? "no-menu" : null)">@RenderBody()</div>
				</div>
			</div>
		} else {
			RenderBody();
		}

In the code above, I located the body tag and then wrapped the contents with an IF statement.  I placed the RenderBody method in the else portion of the statement, and just left everything else in the if portion.


Update the search results page

Modify the resultsList.cshtml file in the "Views\Shared" directory, and locate the table element.  Here we're going to basically do the same thing as in the layout file: wrap the existing content.  Though here I wrapped it with a table, partitioning the content into one column and placing an iframe into the second column.  The URL of that iframe will be the word list file created in the previous steps.  

The code just for the right-column is as follows:

	<td width="10%" valign=top>
	<h3>Search Explorer</h3>
	<div>All the words below exist in the title or notes of your search results.  Click on one word to explore results which contain that word.
	<br><br>
	<script language="JavaScript">
	<!--
	function autoResize(id){
		var newheight;
		var newwidth;
 
		if(document.getElementById){
			newheight=document.getElementById(id).contentWindow.document .body.scrollHeight;
			newwidth=document.getElementById(id).contentWindow.document .body.scrollWidth;
		}
 
		document.getElementById(id).height= (newheight) + "px";
		document.getElementById(id).width= (newwidth) + "px";
	}
	//-->
	</script>
	<iframe id='wordFrame'  src="~/Words?nb=true&q=@HttpContext.Current.Request["q"]" Style="border:0"  onLoad="autoResize('wordFrame');"  />
</td>

Source Files

The entire contents of the changes described above can be downloaded here.  The referenced zip contains only the four files modified, but keep in mind they are for my quick test environment.  You'd need to modify the configuration file to reference your workgroup server, paths, and dataset ID.  Otherwise you can drop them directly into a Webdrawer instance and see the functionality for yourself.