Re-thinking maps in webdrawer

Webdrawer is one possible implementation of a public records portal.  It does its' job and it does it well.  However, it has the distinct disadvantage of having already been built.  I feel like I'm forcing a square map down a round webdrawer (or something like that).  And it doesn't have to be "my way or the highway" with Content Manager! 

First I need to list what I didn't like about my Webdrawer Maps:

  • My map only showed the current page of search results
  • Moving to the next page of search results requires a complete reload of the page 
  • Using the quick search results in a complete reload of the page 
  • Reloading of the page results in loss of other facilities in the map
  • Scrolling around on my map didn't show me other facilities

So I decided to play with something newish: the ServiceAPI.

Here's what I ended up with as a prototype:

2017-09-28_7-52-26.png

Features of this page include:

  1. One page -- the design uses one page that "wires together" the ServiceAPI with Google Maps via jquery
  2. User interface events -- buttons, links, and controls provide interactive experience 
  3. Facilities data -- the list of facilities projected onto the map as markers.  
  4. Facility Info Cards -- clicking a map marker or the "center" link provides the user with a pop-up information window, a distance circle from the marker, and an additional set of search links.  Clicking a search link run a records search within the circle.  

In this post I'll break down the current design and explain how (and why) it was created.


One page

The layout I want is straightforward: I want a map, a search area, and a list of search results.  If I do everything else via jquery then I'll have the responsive design I want.  I've color coded my regions so that you can get an idea what I'm thinking.

Visual styling is intentionally rudimentary since this is not a "real project".

Visual styling is intentionally rudimentary since this is not a "real project".

 
 
  • Yellow -- Search controls
  • Purple -- Loading/busy message
  • Blue -- Search results
  • Pink -- Map coordinates
  • Gray -- Banner

Now I can model this out within a single HTML file, like shown below...

<!DOCTYPE html>
<html>
 
<head>
    <meta name="viewport" content="initial-scale=1.0, user=scalable=no" />
 
    <link href="css/map.css" type="text/css" rel="stylesheet" />
    <!-- Access Google Maps API -->
    <script type="text/javascript" src="https://maps.googleapis.com/maps/api/js?key=donteventhinkaboutit"></script>
    <script type="text/javascript" src="scripts/jquery-3.2.1.js"></script>
    <script type="text/javascript" src="scripts/map.js"></script>
    <script type="text/javascript" src="scripts/cm.js"></script>
    <script type="text/javascript" src="scripts/ui.js"></script>
 
    <!-- Latest compiled and minified CSS -->
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
    <script type="text/javascript">
 
        $(document).ready(function () {
            initializePage();
        });
    </script>
</head>
 
<body>
    <table id="container">
        <tr>
            <td colspan="2" height=30 style="background-color:gray">
                Content Manager Ramble -- ServiceAPI integration with Google Maps
            </td>
        </tr>
        <tr>
            <td width="70%">
                <div id="mapDiv"></div>
                <div id="coordsDiv"></div>
            </td>
            <td>
                <table style="height:100%width100%">
                    <tr height="100">
                        <td align=center valign="middle">
                            <div id="searchDiv">
                                <span>Title/Keyword&nbsp;</span><input id="keywordInput" type="text" /><br><Br><input type=submit id='searchButton' value=Search /><input type="button" id="loadVisibleFacilities" value="Visible in Map" />
                            </div>
                            <div id="errorDiv"></div>
                        </td>
                    </tr>
                    <tr>
                        <td align=center rowspan=2>
                            <div id="loadingDiv"><span class="label label-danger">Danger...Loading!</span></div>
                            <div id="resultsDiv"></div>
                            <div id="notFoundDiv"></div>
                        </td>
                    </tr>
                </table>
            </td>
        </tr>
    </table>
</body>
</html>

That's it!  This HTML file lays out the various elements of my user interface in the form of "<div>"'s. 


User Interface Events

In my previous post I showed how to plot facilities onto a map as the webdrawer page loads.  I want to do something along the same lines here.  Though here I'm going to use the jquery library (not the google maps API) to add some event listeners to the page.

I can think of several events I want to trigger upon:

  1. Load -- once the page is loaded, go fetch all the facilities, mark them on the page, and show the list in the search results page
  2. Click marker -- zoom/center onto the marker and show an information window for the related facility
  3. Click search -- filter the list of facilities to just those included in the search results
  4. Click visible facilities -- filter the list of facilities to just those included in the bounds of the map

If you look back at the HTML code at the top of this post you'll see the javascript code that kicks this whole process off.

 
<script type="text/javascript" src="scripts/jquery-3.2.1.js"></script>
<script type="text/javascript" src="scripts/map.js"></script>
<script type="text/javascript" src="scripts/cm.js"></script>
<script type="text/javascript" src="scripts/ui.js"></script>
<script type="text/javascript">
 
    $(document).ready(function () {
        initializeUI();
    });
</script>

The code above imports a few javascript files I've created and tells the browser to execute "initializeUI" once the document is ready (loaded).  I've included my initialize function below, so that you can see how I've implemented each.

 
function initializeUI() {
    // hide the loading message (since nothing is loading right now)
    var $loading = $('#loadingDiv').hide();
    // when ajax started: tell user via message; when it stops: hide that message
    $(document)
        .ajaxStart(function () {
            $loading.show();
        })
        .ajaxStop(function () {
            $loading.hide();
        });
    // fetch facilities with default search
    loadFacilities();
    // clicking search should apply current search query and fetch
    var searchButton = $('#searchButton').click(function () {
        loadFacilities();
    });
    // clicking visible facilities should filter result list to map bounds
    var loadVisibleFacilities = $('#loadVisibleFacilities').click(function () {
        for (i = 0; i < facilities.length; i++) {
            var mm = facilities[i].mapmarker;
            facilities[i].showInResults = false;
            if (mm != null) {
                var markerpos = mm.getPosition();
                if (map.getBounds().contains(markerpos)) {
                    facilities[i].showInResults = true;
                }
            }
        }
        refreshFacilityList();
    });
}

Facility Data

Now that my design is a one-page HTML file, I need to figure out how to get my data. 

Webdrawer is still a viable option.  I could craft a search string and pass it along into the standard "Record?q=mysearchstringgoeshere" URL.  I could even tack on "&format=json" to get my results without the UI.

 
Webdrawer results page with UI

Webdrawer results page with UI

 
Webdrawer results page without UI

Webdrawer results page without UI

There are a few things I don't like about this approach:

  • It exposes meta-data fields unnecessarily
  • It lets any person see how to manipulate the URL so that different data might be returned

So instead of continuing to add onto webdrawer, I moved it into a separate site called "o1" (option 1).  I then created a new site named "o2" (option 2), which is a duplicate copy of the CM ServiceAPI.  Then I adjusted the hptrim.config file in o2 so that I have routes for "Map", "Facilities", and "Facility".

2017-09-28_9-37-19.png

This means I don't have to worry about someone altering the URL and exfiltrating records I don't want them to have. 

Altering the URL does not expose the document I created today

Altering the URL does not expose the document I created today

It also means my user interface can retrieve data from the "/Facilities?format=json" URL

2017-09-28_9-39-24.png

Great, so now I've got a single web-page partitioned into a few user interface elements.  I've also got a Json data service exposing just a list of facililties, and only the fields I need for my user interface.  Time to wire it up!


Ajax loading

Now that I have my one-page user interface "wired-up" to do things, I need to tell it how to actually do those things.  First things first.... I need to load my facilities.  To accomplish that I'll craft a query string (based on the current state of the user interface), disable the search controls, and initiate an ajax call.

function loadFacilities() {
    qs = generateQuery();
    if (isEmpty(qs)) {
        enableSearch();
    } else {
        disableSearch();
        ajaxActive = true;
        $.ajax({
            url: 'http://wg1/o2/Facilities',
            type: 'GET',
            data: { q: qs, format: 'json', pagesize: 5000, start: 0 },
            contentType: 'text/html; charset=utf-8',
            success: function (returnData) {
                updateResults(returnData);
            },
            error: function (err) {
                displayError(err.statusText);
            },
        }).always(function (jq, ts) {
            if (ts != "success") {
                displayError(jq.statusText);
            }
            enableSearch();
        });
    }
}

In the above code I fire-off a request to my new "o2/Facilities" data service (provided by the ServiceAPI).  When I get a successful response I "updateResults".  When I get an error I "displayError".  Either way, I re-enable the search interface once completed.

Within this implementation I want to place all facilities on the map, but only selectively display them in the search results list.  When a user searches, I will toggle the display in the list (or on the map) based upon the later results.  By taking this approach, I reduce the constant loading/unloading of facility information and improve the overall user experience.

for (i = 0; i < facilities.length; i++) {
    facilities[i].showInResults = false;
}
        // import list of facilities matching search criteria
$.each(obj.Results, function (key) {
    var rec = obj.Results[key];
    var found = false;
    for (i = 0; i < facilities.length; i++) {
        if (facilities[i].uri == rec.Uri) {
            facilities[i].showInResults = true;
            found = true;
            break;
        }
    }
    if (!found) {
        var facility = { "uri": rec.Uri, "number": rec.RecordNumber.Value, "title": rec.RecordTitle.Value, "latlng": rec.RecordGpsLocation.Value, "mapmarker"null, showInResults: true };
        facilities.push(facility);
        addMapMarker(facility);
    }
});

 

That's it!  Now if the user clicks "search", the ajax call will get back a list of facilties matching the new search query.  The function above will "hide" all facilities from the search results and "show" only those in the results.  I like this approach because it means I can manipulate the map for items not included in the search results.  I'll need that capability when I start adding map layers.


That's it!  Now I've got a fully functional single HTML page that can interact with both the ServiceAPI and Google Maps.  More importantly, it doesn't expose any data out via webdrawer.  Lastly, I'm now prepared to implement my "search within this circle" concept via the map.