DDJM 3

Geographic Data Delivery

Add data driven journalism to your project using Sophora's geographic delivery.

The geographic data delivery project is an extension to the regular delivery project and provides everything needed to display the geographic data tables on a page in an interactive manner.

It supports multiple popups, hovering, live-filtering and a web accessible view of the overall table. The following documentation describes the essential geodata interfaces on the server side as well as on the client side.

Requirements

  • After installing the geodata delivery addon in your DeskClient, make sure to have at least one node type configured to use the mixin sophora-geodata-mix:geoDataMap. For the sake of simplicity a sample node type will be called sophora-demo-nt:map throughtout the rest of this document.
  • Java-Servlets version 3 or newer
  • jstl-tag-library
  • Spring Web
  • A Bing Maps Key

Components

The geographic data delivery relies on a javascript library - based on OpenLayers3 - and a servlet, which needs to be added to your web.xml. Furthermore a couple of jsp-files is required for providing required HTML-DOM-Structure.

Setup

Apache Configuration

The GeoDataServlet provides several paths that are needed by the JavaScript library.

So in case an Apache Webserver is mounted, make sure that all requests to the servlet are forwarded to the Tomcat server. This might be accomplished by using a rewrite rule like this

# Geodata Servlet
RewriteCond %{SCRIPT_FILENAME} ^/.*geodata\.servlet(.*)$
RewriteRule ^/(.*)$ /my-webapp/example/$1 [PT]

Configuring the Webapp

Sophora Properties

The geographic data delivery uses Sophora functions from the package com.subshell.sophora.geodata.delivery.functions. This therefore needs to be added to the key sophora.delivery.function.packageNames.

sophora.delivery.function.packageNames=com.example.myproject.functions,com.subshell.sophora.geodata.delivery.functions

Furthermore a Bing Maps Key is needed. This should be configured as shown below:

sophora.delivery.geodata.bing.api.key=YOUR_BING_KEY

The Bing Maps Key can be configured using a DeskClient configuration entry as well. The key needs to be called com.subshell.sophora.eclipse.map.credentials while the sole value is your Bing Maps Key.

Maven

Add the project com.subshell.sophora.geodata.delivery to your maven dependencies.

<dependency>
	    <groupId>com.subshell.sophora</groupId>
        <artifactId>com.subshell.sophora.geodata.delivery</artifactId>
        <version>2.2.11</version>
</dependency>

web.xml

The web.xml must hold the GeoDataServlet an map it to a generic path with a trailing *-sign. For enabling proper Spring-Bean support in Sophora functions a custom ContextLoaderListener needs to be used.

If your project yet uses org.springframework.web.context.ContextLoaderListener as a listener then just replace it with com.subshell.sophora.delivery.api.context.DeliveryContextLoaderListener.

Otherwise add it as shown below an make sure that an application content configuration file is added.

<context-param>
  <param-name>contextConfigLocation</param-name>
  <param-value>/WEB-INF/classes/applicationContext.xml</param-value>
</context-param>

<listener>
  <listener-class>com.subshell.sophora.delivery.api.context.DeliveryContextLoaderListener</listener-class>
</listener>

<servlet>
	<servlet-name>GeoDataServlet</servlet-name>
	<servlet-class>com.subshell.sophora.geodata.delivery.servlet.GeoDataServlet</servlet-class>
</servlet>

<servlet-mapping>
	<servlet-name>GeoDataServlet</servlet-name>
	<url-pattern>/system/servlet/geodata.servlet/*</url-pattern>
</servlet-mapping>

Application Context Configuration

Geographic data delivery uses spring bean autowiring and a central bean configuration class, so your bean configuration file needs to contain the following lines:

<context:annotation-config /> 
<bean class="com.subshell.sophora.geodata.delivery.config.GeoDataBeanConfig"/>

templates.xml

This step depends on your general project setup and may vary from case to case. For the map to work correctly it is important to have the popup ('popup') and barriere free ('table') template type connected to the same nodetype ('sophora-demo-nt:map') as the map template type ('default'). The name for the nodetype and the template types depend on your setup.

<nodetype name="sophora-demo-nt:map">
	<templateset extends="default">
		<template type="default" model="basemodel">/path/to/map.jsp</template>
		<template type="table" model="basemodel">/path/to/barrierfree.jsp</template>
		<template type="popup" model="basemodel">/path/to/popup.jsp</template>
	</templateset>
</nodetype>

Required Implementation

Minimal JSP Example of the main map view

This minimal example does not include any styling for the map.

<%@ page pageEncoding="utf-8" contentType="text/html; charset=UTF-8"%>
<%@ taglib uri="http://www.subshell.com/sophora/jsp" prefix="sophora"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%>
<%@ taglib uri="http://www.subshell.com/sophora/geoData/jsp" prefix="geoData" %>

<link rel="stylesheet"	href="<sophora:staticUrl path="/common/js/openlayers/dist/ol.css" sitePrefix="/." />" type="text/css" />
<script src="<sophora:staticUrl path="/common/geodata/jquery/dist/jquery.min.js" sitePrefix="/."/>" type="text/javascript"></script>
<script src="<sophora:staticUrl path="/common/geodata/openlayers/dist/ol.js" sitePrefix="/."/>" type="text/javascript"></script>
<script src="<sophora:staticUrl path="/common/geodata/geodata.min.js" sitePrefix="/."/>" type="text/javascript"></script>

<!-- filters container -->
<nav id="geoDataFilters"></nav>

<!-- map -->
<div id="geoDataMap"></div>

<!-- small popup containing the title -->
<div id="geoData-small-popup" class="sophora-geodata-popup">
	<div class="sophora-geodata-popup-content"></div>
</div>

<!-- big popup -->
<div id="geoData-big-popup" class="sophora-geodata-popup">
    <a href="#" class="sophora-geodata-popup-closer">x</a>
	<div class="sophora-geodata-popup-content"></div>
</div>

<!-- legend -->
<c:set var="legend" value="${current.getLegend}"></c:set>
<c:if test="${not empty legend}">
	<div>${legend}</div>
</c:if>

<script type="text/javascript">
	(function() {
	  	var settings = {
	  		servletPath: '<sophora:staticUrl path="/system/servlet/geodata.servlet" sitePrefix="/./"/>',
	  		popupPathTemplate: '<geoData:popupPath templateName="popup" />'
	  	};

		var elements = {
		    popups: {
			    smallId: 'geoData-small-popup',
			    bigId: 'geoData-big-popup'
			},
			containerId: 'geoDataMap',
		 	filterContainerId: 'geoDataFilters'
		};

		var config = ${current.getGeoDataConfig.asJson()};

		var geoDataMap = new SophoraGeoData.Map(settings, elements, config);
	})();
</script>

In the likely case that your web app yet includes jQuery the following line can be removed:

<script src="<sophora:staticUrl path="/common/geodata/jquery/dist/jquery.min.js" sitePrefix="/."/>" type="text/javascript"></script>

The geodata javascript library requires jQuery to be accessible through the variable $. If your webapp yet uses a different name for jQuery then add this line prior to creating the SophoraGeoData.Map javascript object:

window.$ = yourGlobalJQueryVariable;

JS and CSS Resources

There are several required and optional CSS and JS resources inside the META-INF/resources/common/geodata folder.

  • geodata: geodata.js and geodata.min.js
  • jquery: jquery/dist/jquery.min.js
  • openlayers: openlayers/dist/ol.js and openlayers/dist/ol.css
  • openlayers debug: openlayers/dist/ol-debug.js and openlayers/dist/ol-debug.css

HTML Elements

Filter

At runtime on the client side the map will generate a list of all filters and filter values. The HTML element containing all filters will be added to the HTML element you have passed into the SophoraGeoData.Map as the second argument.

When a filter value is clicked on the following changes occure inside the HTML:

  • When a filter value is selected it receives the CSS class sophora-geodata-active
  • When one or more of the filter values inside a filter are active the parent filter recieves the CSS class sophora-geodata-active-children
The following HTML code shows a sample filter list. Note that the nav HTML element is is the element one passes into SophoraGeoData.Map and therefore is not part of the generated HTML.

<nav id="geoDataFilters">
 <ul class="sophora-geodata-filter-list">
 	 <!-- Filter -->
 	 <li id="geoDataFilter_animal" class="sophora-geodata-filter">
 		<a href="">Animal</a>
 		
		<!-- Filter Values -->
		<ul class="sophora-geodata-filter-value-list">

			<!-- Filter Value -->
			<li class="sophora-geodata-filter-value">
				<a href="#" class="sophora-geodata-filter-value-switch ">
					<label for="geoDataFilter_animal_value__dog_"> 
					<input type="checkbox" id="geoDataFilter_animal_value_dog_" index="0" /> 
					Dog <span class="sophora-geodata-filter-value-amount">(2 / 354)</span>
					</label> 
				</a>
			</li>

			<!-- Filter Value --> 
			<li class="sophora-geodata-filter-value">
				<a href="#" class="sophora-geodata-filter-value-switch ">
					<label for="geoDataFilter_animal_value__cat_"> 
					<input type="checkbox" id="geoDataFilter_animal_value_cat_" index="1" /> 
					Cat <span class="sophora-geodata-filter-value-amount">(6 / 686)</span> 
					</label> 
				</a> 
			</li> 
		</ul> 
	</li> 
</ul> 
</nav>
Legend

Unlike the filter list the legend is not generated through java script. It is provided via the node function current.getLegend.

This node function generates an HTML snippet with an unordered list. If an optional legend info text exists a <span> with the css class sophora-geodata-legend-info-text will be prepended. For pin-based maps a sample legend might look like this:

<span class="sophora-geodata-legend-info-text">Here is the legend info text.</span>
<ul>
	<li><span class="sophora-geodata-legend-icon"><img alt="red pin" src="pin1-100~_v-original.png"></span><span class="sophora-geodata-legend-desc">Red</span></li>
	<li><span class="sophora-geodata-legend-icon"><img alt="white pin" src="pin2-100~_v-original.png"></span><span class="sophora-geodata-legend-desc">White</span></li>
	<!-- ... -->
</ul>

When the map uses shapes the return value of the node function current.getLegend will be slightly different:

<ul>
	<li><span class="sophora-geodata-legend-icon"><div style="width:30px;height:30px;background-color:rgba(44,163,3,0.8);border-color:rgb(109,109,54);border-width:2px;border-style:dotted;"></div></span></li>
    <!-- ... -->
</ul>
Search Slot

If the map is configured to provide a search slot then a <div>-object of this type will be created.

<div class="sophora-maps-search-container">
  <input class="sophora-maps-search-input">
  <div class="sophora-maps-search-result-container">
   <ul class="sophora-maps-search-result"></ul>
  </div>
</div>

When a search is performed an according number of <li>-Elements is added to the list elements. Each of these elements looks like this:

<li class="sophora-maps-search-result-entry">NAME</li>

These items might be selected using keyboard or mouse. In case controlling with keyboard is done they will get the addition class sophora-maps-search-item-active will being active.

Besides these classes that should be used for proper styling, the map object itself provides a couple of JavaScript functions for triggering a search or reacing to it. They are listed as part of the JavaScript API in the section below.

Map Interaction

Whenever the user moves the cursor across the map and hits a feature, a pin for example, the css class sophora-geodata-feature-hover is applied. Using this class the style of the cursor can easily be changed.

.sophora-geodata-feature-hover {
	cursor: pointer;
}

Popups

There are two types of popups: a small popup which shows up whenever the user hovers a feature such as a pin and a detailed popup which shows up when the user clicks a feature. Both popups need an HTML prototype element.

As for other HTML containers used by the JavaScript the prototype is refered to by its unique id. Both prototypes need an element with the class sophora-geodata-popup-content. Basically the dynamic content of the element is loaded into this specific HTML element.

The big popup might also have an element with the class sophora-geodata-popup-closer which when clicked closes the current popup. The content of the big popup is loaded via AJAX - the destination it is loaded from is passed into the SophoraGeoData.Map in the settings object (popupPathTemplate).

Big Popup Example

<%@ page pageEncoding="utf-8" contentType="text/html; charset=UTF-8"%>
<%@ taglib uri="http://www.subshell.com/sophora/geoData/jsp" prefix="geoData" %>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%>
<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>
<%@ taglib uri="http://www.subshell.com/sophora/jsp" prefix="sophora" %>
<%@ taglib tagdir="/WEB-INF/tags/gsea-commons" prefix="gc"%>

<geoData:getPopupContent var="popupContent" uuid="${current.uuid}" index="${urlParam.index}"/>

<div style="max-height: 400px; overflow-y: scroll; width: 450px; padding: 5px;">
<%-- headline --%>
<c:set var="headline" value="${popupContent.getHeadLine()}"/>
<c:if test="${headline.isPresent()}">
	<h2><c:out escapeXml="false" value="${headline.get()}"></c:out></h2>
</c:if>

<%-- teaser image --%>
<c:set var="teaserImage" value="${popupContent.getTeaserImageExternalId()}"/>
<c:if test="${teaserImage.isPresent()}">
	<sophora:getContentMap var="teaserDoc" externalid="${teaserImage.get()}" />
	<c:set var="image" value="${empty teaserDoc['teaserImage'] ? teaserDoc : teaserDoc['teaserImage']}" />

	<c:if test="${not empty image}">
		<sophora:map var="params" v="original" />
		<sophora:url var="teaserImageURL" externalId="${image['sophora:externalId']}" urlParams="${params}"	suffix="png" />
		<img class="teaser" src="${teaserImageURL}" />
	</c:if>
</c:if>

<%-- playout popup value pairs --%>
<c:set var="playoutValues" value="${popupContent.getPlayoutPopupValuePairs()}"/>
<c:forEach items="${playoutValues.keySet()}" var="key">
	<c:set var="currentValues" value="${playoutValues.get(key)}"/>
	<span class="title"><strong>${key}</strong></span> 
	<c:choose>
		<c:when test="${currentValues.size() > 1}">
			<ul>
				<c:forEach items="${currentValues}" var="currentValue">
					<li><c:out escapeXml="false" value="${currentValue}"></c:out></li>
				</c:forEach>
			</ul>
		</c:when>
		<c:otherwise>
			<p><c:out escapeXml="false" value="${currentValues.iterator().next()}"></c:out></p>
		</c:otherwise>
	</c:choose>
</c:forEach>
</div>

Note that popups won't be shown, if no column of the corresponding data table as shown in the desk client is configured to be contained in the mouseover or popup. In the latter case, the maps features won't be clickable.

A map might be configured to not open popups. If this is the case, then only data lines containing a linked document will be clickable and upon click will open a new tab with the linked document rather then opening a new tab.

Web accessible view

The web-accessible view is created with the jsp-tag getWebAccessibleData. It provides a complex Java object for displaying a table-like view of the data. A page using this tag might look like this:

<%@ page pageEncoding="utf-8" contentType="text/html; charset=UTF-8" %>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core"prefix="c" %>
<%@ taglib uri="http://www.subshell.com/sophora/geoData/jsp" prefix="geoData" %>

<c:set var="mapName" value="${current['sophora-geodata:name']}"/>
<h3 class="headline small"> ${mapName} (accessible view)</h3>
<c:set var="aboutThisMap" value="${current.getString('sophora-geodata:aboutThisMap')}"/>
<c:if test="${not empty aboutThisMap}">
  <p class="einleitung small">${aboutThisMap}</p>
</c:if>

<geoData:getWebAccessibleData var="accessibleModel" uuid="${current.uuid}" linkTemplateType="tableLink"/>
<table>
  <thead>
    <tr class="headlines">
      <c:forEach items="${accessibleModel.columnNames}" var="columName">
        <c:choose>
          <c:when test="${accessibleModel.isColumnSortable(columName)}">
            <th class="entry sortable">${columName}</th>
          </c:when>
          <c:otherwise>
            <th class="entry">${columName}</th>
          </c:otherwise>
        </c:choose>
      </c:forEach>
    </tr>
  </thead>
  <tbody>
    <c:forEach items="${accessibleModel.tableRowValues}" var="row">
      <tr class="data">
	    <c:forEach items="${row}" var="cell">
          <td class="entry">${cell}</td>
        </c:forEach>
      </tr>
    </c:forEach>
   </tbody>
</table>

Note that the property Sortable on columns that are shown in accessible mode is just provided here through the method isColumnSortable(name). Sorting is not performed by the tag <geoData:getWebAccessibleData> directly. Instead we propose implementing this with JavaScript as part of the webapp based on the information provided by the accessible model.

If you have a column of type document reference contained within the map's web accessible view, some additional handling is required. The accessible data model will just create a SSIs for the referenced documents using a configurable template type (tableLink in the above example, which is the default as well).

Make sure that all document types you want to be referred to from geo data tables have a template for this type. The easiest way to achive this is to just add it to a default template set like this:

<templateset name="default">
    ....
    <template type="tableLink">/common/templates/miniTeaser.jsp</template>
</templateset>

A such miniTeaser.jsp then might look like this:

<%@ page pageEncoding="utf-8" contentType="text/html; charset=UTF-8" %>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core"			 prefix="c" %>
<%@ taglib uri="http://www.subshell.com/sophora/jsp" prefix="sophora" %>

<sophora:url var="url" sophoraId="${current.id}" />
<c:set var="title">
    <c:choose>
        <c:when test="${not empty current['sophora-content:title']}">${current['sophora-content:title']}</c:when>
 		<c:otherwise>${current.id}</c:otherwise>
 	</c:choose>
</c:set>

< a href="${url}" target="_blank">${title}</a>

More complex examples might create teasers or use different logic for each node type.

In case you place a link to the web accessible page you might want to use the tag yet on the calling page and check whether there is any accessible data to show at all:

<geoData:getWebAccessibleData var="accessibleModel" uuid="${current.uuid}"/>
<c:if test="${accessibleModel.containsData}">
...INSERT LINK
</c:if>

Overview of all provided interfaces

JSP Taglib

To use the taglib include <%@ taglib uri="http://www.subshell.com/sophora/geoData/jsp" prefix="geoData" %> into the JSP file.

JSP TagAttributesDescription
getPopupContentuuid: String (required)
index: Integer (required)
Returns the content of the Popup (GeoDataPopupContent). The GeoDataPopupContent provides access to the following methods:
getHeadline(): Optional<String>
getTeaserImageExternalId(): Optional<String>
getPlayoutPopupValuePairs(): Multimap<String, String>
getTeaserExternalId(): Optional<String>
getTeaserLinkExternalIds(): List<String>
getIndex(): int - this is the number of the line from the table that corresponds the clicked feature
getWebAccessibleDatauuid: String (required)
valueSeparator: String (not required)
linkTemplateType: String
Returns a WebAccessibleGeoData object for the geodata table from the document with the given uuid that allows to display the columns of the map that are marked as accessible.
getContainsData(): boolean - This informs, whether there is any accessible data at all
getColumnNames(): List<String> - The names of all columns that contain web accessible data, should be used to fill table head elements
getTableRowValues(): List<List<String>>String representations of the values, group by the column names, should be used to fill table cells
isColumnSortable(String: name): boolean - This returns whether the column with the passed name (as returned by getColumnNames()) should be sortable. Use it to add proper classes or attributes to support sorting using JavaScript. (Requires version 2.6.0, 2.5.5, 2.4.10 or 2.3.16 of com.subshell.sophora.geodata.delivery)

The valueSeparator is used to join values from multi-type columns.
The linkTemplateType defines the template name for SSIs to document references
popupPathtemplateName: String (not required, default: 'popup')Returns the popup path template which is required by the SophoraGeoData.Map constructor in the settings object with the key popupPathTemplate. (See also: templates.xml)

Node Functions

Node FunctionReturn TypeDescription
getLegendStringReturns the HTML of the legend for the current map.
getGeoDataConfigGeoDataConfigDTOReturns the config object whose json representation has to be passed into the SophoraGeoData.Map function as the third argument. To get the JSON representation of the config, call the method asJson on the config object.
Example var config = ${current.getGeoDataConfig.asJson()};

JavaScript API

The geographic delivery provides a rich amount of JavaScript interfaces to interact with the map or the filter.

The following table is an overview about all functions. All functions are called directly on the SophoraGeoData.Map instance. A few usecases are listed below.

Filter Data Model
Data ModelFields
Filterkey: string
displayName: string
values: FilterValue[]
FilterValuekey: string
displayName: string
hits: number
total: number
selected: boolean
LocationSearch Model
Data ModelFields and functions
LocationSearchapply(): () => void selects this result
result: LocationSearchResultData
LocationSearchResultDataname: string
relevance: number
boundingBoxTopLeft: [number, number] ([lat, lon])
boundingBoxBottomRight: [number, number] ([lat, lon])
point: [number, number] ([lat, lon])
API General Functions
FunctionReturn TypeDescription
repaintMap()voidRepaints the map. Usually this function needs to be called when the map is resized.
getFilters()FilterReturns an array of all Filters.
getActiveFilters()Filter[]Returns a subset of all Filters where at least one FilterValue is selected.
getFilterElement(filter: Filter | string)JQueryElementGiven a Filter or a filter key this function will return the corresponding html element of the Filter wrapped in a JQuery object.
getFilterValueElement(filter: Filter | string, filterValue: FilterValue | string)JQueryElementGiven a Filter or a filter key and a FilterValue or a filter value key this function will return the corresponding html element of the FilterValue wrapped in a JQuery object.
setFilterValue(filter: Filter | string,
filterValue: FilterValue | string, status: boolean = true)
voidGiven a Filter or a filter key and a FilterValue or a filter value key this function set the FilterValue status to the provided status.
resetFilters()voidResets all Filters or more specific all FilterValues to false. To set or reset a specifc FilterValue please use the function setFilterValue.
resetFilter(filter: Filter | string)voidResets a specific Filter and all its FilterValues.
search(text: string, onResult: (LocationSearch[]) => any)voidTriggers a search with the given search text. The Listener onResult is called with an array of resulting LocationSearch objects when results are available.
API Listeners
FunctionReturn TypeDescription
onFiltersChanged(listener: (Filter[]) => void, thisArg?: any)voidRegisters a listener which is called after any filter has changed. For convenience the callback function recieves the array of the current filters. (Optional thisArg parameter to set the this context)
onFiltersRendered(listener: (Filter[]) => void, thisArg?: any)voidRegisters a listener which is called after any filter has redered. For convenience the callback function recieves the array of the current filters. Most of the time it is recommended to use the onFiltersChanged function. (Optional thisArg parameter to set the this context)
onMapInitialized(listener: () => void, thisArg?: any)voidRegisters a listener which is called after the map is initialized. The map is initialized when the openlayers container is ready and the inital geographic data (e.g. pins or shaped) is fetched from the server and has been displayed. (Optional thisArg parameter to set the this context)
onSearchResultsChanged(listener: (data: LocationSearchResultData[]) => void, thisArg?: any)voidRegisters a listener that is called whenever search results have been received. The LocationSearchResultData found by Bing are provided. (Optional thisArg parameter to set the this context)
onSearchResultSelected(listener: (data: LocationSearchResultData) => void, thisArg?: any)voidRegisters a listener that is called whenever a search result is selected by the user. The selected LocationSearchResultData object is provided. (Optional thisArg parameter to set the this context)

Using the JavaScript API

// showing a success message after the map has been initialzed
var map = new SophoraGeoData.Map();
map.onMapInitialzed(function () {
	alert('map initialized');
});
// setting the value of the first filter value of the first filter to true
var map = new SophoraGeoData.Map(); 
var filters = map.getFilters();
var filter = filters[0];
map.setFilterValue(filter, filter.filterValues[0], true);

Multiple Maps Within One Page

Using multiple maps on one page is entirely possible. The only challenge is to reference the elements on each map, hence their ids, individually. This can easily be solved by using a random suffix for each map.

The random suffix has to be added to all HTML container element ids (map, filter, small and big popup). In other words: Each map needs its own unique id suffix which will be used for all its container elements.

The following JSP code will generate a unique number:

<c:set var="randomID">
  <% String.format("%x", java.lang.Math.round(java.lang.Math.random() * 1791) + 256) %>
</c:set>

Last modified on 3/9/20

The content of this page is licensed under the CC BY 4.0 License. Code samples are licensed under the MIT License.

Icon