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.

Geolocations via Content Manager

Before I go through the trouble of writing more code, I should take a look at what's available.  Hopefully by reviewing each component I can make a list of features missing.  Then I can decide how best to cobble together my final solution.


Accessing Facilities via the Thick Client

When performing a search it is easy to specify that the results include the GPS location in both the view pane and the list pane.  Making the fields visible lets me peruse the data and verify I like the results.  There's no way for me to leverage that data here without a mapping interface.

 
2017-09-24_14-44-29.png

I can use the view pane's GPS Location hyperlink to browse to the location via the map connector.

 
2017-09-24_14-49-37.png

But that's it.  Only potential thing I see here is swapping out the static google map connector for one that embeds an iframe showing results nearby.


Viewing Facilities via Web Client

The out-of-the-box configuration of the Web Client does not support a grid style layout for meta-data in the search results pane.  As you select an individual record in the results, the right-hand view pane exposes a link to the location.  

 
2017-09-24_14-38-28.png

To modify the value you'd either get to the registration form via the update button...

 
2017-09-24_20-00-31.png

Or you click the GPS Location option from the Details drop-down button

 
2017-09-24_15-02-52.png
2017-09-24_15-01-35.png

That's it.  Nothing else to see here! :(


View Facilities via WebDrawer

Webdrawer is a no-thrills interface for the exposure of public records.  Using it implies (at least to me) that you don't care who the user is and you'll just be reading data.  That fits for my scenario, so let's see what's available.

 
2017-09-24_20-06-32.png

Entering "**" into the quick search box and pressing enter should result in all facility boxes being returned (I have nothing else in the dataset).  The search results page shows my boxes, but nothing useful to mapping.  

 
2017-09-24_20-07-44.png

Clicking a facility box yields no mapping fields or data either.  I tried expanding and collapsing all of the regions but no dice.

 
2017-09-24_20-09-32.png

 A quick check of the Webdrawer installation manual and the HPE support site turns up no hits for various relevant keywords. So I visited the ServiceAPI's Property and Field Definition page and see that "RecordGpsLocation" is the property to include in route details.  I add it to both the routes for the search results (WDRecordList) and record details (WDRecordDetails) templates.

 
Note that this is the webdrawer configuration file of a remote server

Note that this is the webdrawer configuration file of a remote server

Modifying the configuration file should result in an automatic reload of the IIS application.  A refresh of my search results page gives me the result shown.

 
2017-09-24_20-27-56.png

A refresh of my record details page does not result in seeing the property.  That's because I added it to the route (which funnels the data in the template), but I did not include it in any of the custom property sets used to group items into the accordion views.  Adding the property to the custom record identification property set should solve the issue.

 
2017-09-24_20-30-00.png

Refreshing the page gave me a quick laugh.  I wish the GPS location was a hyperlink!

 
2017-09-24_20-33-03.png

I'm sure there's a better way to handle this, but I found it easiest to go into the propertyRow partial template and wrap the property value output in a condition.  If the value starts with "https://www.google.com/maps" then replace the output with a valid link.  Otherwise use the normal condition.  Definitely not an ideal solution.

 
2017-09-24_20-43-04.png

Now a reload of the page gives me a usable link.  

 
2017-09-24_20-49-41.png

What's missing?

I have no way, out-of-the-box, to mark search results within the extent of one map.  So re-creating my goal is not possible without more code.  That's ok though.  I've got this! :)

Using Xsd to Dynamically change DataPort during install

This post will lead to code which can manipulate the list of available source format in Content Manager's DataPort.  The code is executed as the last action within an installer project for a new data provider.  It loads an Xml document representing the user's list of sources and then either adds to the list or updates existing entries.  During uninstall it will remove only the newly installed source.

What I needed to accomplish this:

  • Microsoft Visual Studio 2017
  • .Net Framework 4.5.2
  • WiX Toolset v3
  • HPE Content Manager 9.1

The user's list of available DataPort sources is located off the roaming user profile.  Within it exists one node typed "ArrayofDataFormatterDefinition".  That, in turn, contains one or more DataFormatterDefinition children.  

2017-09-24_6-30-50.png

The goals include: add, update, or remove items from this configuration file.


First I launched the Developer Command Prompt for Visual Studio 2017 so that I could mirror someone else's model within my own project.

2017-09-24_4-51-58.png

I navigated into the roaming application data directory for data port preferences.  Executing XSD within that starting directory will make it easier to organize my results.

2017-09-24_4-56-36.png

We can only use XSD on files ending with an ".xml" extension, which the developers have curiously not done.  Since I also don't want to mess up my own copy some how, I might as well go ahead and copy what I've got to a file XSD will accept.  

I did this by executing

copy ImportDataFormatters ImportDataFormatters.xml

Then I execute

xsd ImportDataFormatters.xml

Command Prompt after generating scheme definition

Command Prompt after generating scheme definition

Next I execute

xsd ImportDataFormatters.xsd /c

Command Prompt after generating class definition

Command Prompt after generating class definition

Next I flipped over to Visual Studio and imported the class file.

2017-09-24_5-44-17.png
2017-09-24_6-13-08.png

The file name doesn't match the generated class names.  It doesn't matter either.  What I really want are the properties of the second class defined.  These are the things I want to change for the user. 

2017-09-24_6-15-42.png

In my custom action for this installer I can now serialize and deserialize using the code below.

private static void SaveImportFormattersPreferenceFile(string preferenceFile, XmlSerializer serializer, ArrayOfDataFormatterDefinition importFormatters)
{
    using (TextWriter writer = new StreamWriter(preferenceFile))
    {
        serializer.Serialize(writer, importFormatters);
        writer.Close();
    }
}
 
private static ArrayOfDataFormatterDefinition LoadImportFormattersPreferenceFile(string preferenceFile, XmlSerializer serializer)
{
    ArrayOfDataFormatterDefinition importFormatters;
    using (StreamReader reader = new StreamReader(preferenceFile))
    {
        importFormatters = (ArrayOfDataFormatterDefinition)serializer.Deserialize(reader);
        reader.Close();
    }
 
    return importFormatters;
}

Next I need the logic to find entries in the list or to create a new one.

XmlSerializer serializer = new XmlSerializer(typeof(ArrayOfDataFormatterDefinition));
ArrayOfDataFormatterDefinition importFormatters = LoadImportFormattersPreferenceFile(preferenceFile, serializer);
List<ArrayOfDataFormatterDefinitionDataFormatterDefinition> items = importFormatters.Items.ToList();
var item = importFormatters.Items.FirstOrDefault(x => x.ClassName.Equals("CMRamble.DataPort.Acme"));
if (item == null)
{
    item = new ArrayOfDataFormatterDefinitionDataFormatterDefinition();
    items.Add(item);
}

After I'm done manipulating the item I have in memory, I need to save the changes to disk.

importFormatters.Items = items.ToArray();
SaveImportFormattersPreferenceFile(preferenceFile, serializer, importFormatters);

During my uninstall action I need to basically repeat the process, but this time just remove anything matching my class name.

XmlSerializer serializer = new XmlSerializer(typeof(ArrayOfDataFormatterDefinition));
ArrayOfDataFormatterDefinition importFormatters = LoadImportFormattersPreferenceFile(preferenceFile, serializer);
List<ArrayOfDataFormatterDefinitionDataFormatterDefinition> items = importFormatters.Items.ToList();
importFormatters.Items = items.Where(x => !x.ClassName.Equals("CMRamble.DataPort.Acme")).ToArray();
SaveImportFormattersPreferenceFile(preferenceFile, serializer, importFormatters);

And that's it!  The above code can be attached to any WiX installer action.