Java EE 8 MVC + Leaflet Map Demo

This article shows how to create a geographic map using Java EE 8 MVC and Leafleft. My goal is to replace this static boring map with something more sexy. I used the new Java EE MVC and Leaflet for this stuff.

Java EE's new action-based MVC framework will be available in Java EE 8. You can get basic information about Ozark (MVC reference implementation) from this article. Leaflet is an open-source JavaScript library for interactive maps.

For application development, I use Glassfish 4.1.1 from Java EE SDK update 2. For running the app in production, I used Tomcat 7.

Architecture

Application is divided (as MVC design pattern requires) to three layers:

Application

Java EE MVC is based on JAX-RS. So creating an application is simple using annotation:

@ApplicationPath("ozark")
public class GettingStartedApplication extends Application 
{
}

Controller

Data about air quality (shortly AQ) that I want to display on map is in this JSON file, updated every hour. You can use an online JSON viewer to discover its structure. There is a legend (air quality index) and information about measuring stations and air quality index stated here.

The role of Controller is to read and parse this JSON and populate required information to MapModel that is injected by CDI.

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.URL;
import java.net.URLConnection;
import java.nio.charset.StandardCharsets;

import javax.inject.Inject;
import javax.json.Json;
import javax.json.JsonArray;
import javax.json.JsonObject;
import javax.json.JsonReader;
import javax.mvc.annotation.Controller;
import javax.ws.rs.GET;
import javax.ws.rs.Path;

/**
 * The MVC Controller. It waits for HTTP GET requests on his Path: 
 * <code>http://host:port/AppName/@ApplicationPath/@Path</code>
 * 
 * @author lvanek
 *
 */
@Controller
@Path("map")
public class MapController 
{
    /**
     * The MVC model
     */
    @Inject
    MapModel mapModel;


    /**
     * Method responds to HTTP GET requests
     * 
     * @return View path
     */
    @GET
    public String map()
    {
    createModel();

    return "/WEB-INF/jsp/mapView.jsp";
    }

    /**
     * Read JSON with Air quality data and populate MVC model
     * 
     * Use:
     * <code>http://jsonviewer.stack.hu/#http://portal.chmi.cz/files/portal/docs/uoco/web_generator/aqindex_eng.json</code><p>
     * for JSON browsing
     * 
     */
    private void createModel()
    {
    try
    {
    URL vvv = new URL("http://vvv.chmi.cz/uoco/aqindex_eng.json"); 
    URLConnection urlConnection = vvv.openConnection();

    try (final BufferedReader reader = new BufferedReader(new InputStreamReader(urlConnection.getInputStream(), StandardCharsets.UTF_8.displayName()))) 
    {
    JsonReader jsonReader = Json.createReader(reader);
    final JsonObject root = (JsonObject) jsonReader.read();

    mapModel.setActualized(root.getString("Actualized"));

    final JsonArray jsonLegend = root.getJsonArray("Legend");

    for (int i = 0; i < jsonLegend.size(); i++)
    {
    JsonObject jsonAqIndex = jsonLegend.getJsonObject(i);

    mapModel.putAirQualiryLegendItem(jsonAqIndex.getInt("Ix"), new AirQualiryLegendItem(jsonAqIndex.getInt("Ix"), 
    jsonAqIndex.getString("Color"), 
    jsonAqIndex.getString("ColorText"), 
    jsonAqIndex.getString("Description")));
    }

    final JsonArray jsonStates = root.getJsonArray("States");

    for (int s = 0; s < jsonStates.size(); s++)
    {
    final JsonObject jsonState = jsonStates.getJsonObject(s);
    final JsonArray jsonRegions = jsonState.getJsonArray("Regions");

    for (int i = 0; i < jsonRegions.size(); i++)
    {
    final JsonObject jsonRegion = jsonRegions.getJsonObject(i);

    final JsonArray jsonStations = jsonRegion.getJsonArray("Stations");
    for (int j = 0; j < jsonStations.size(); j++)
    {
    final JsonObject jsonStation = jsonStations.getJsonObject(j);

    if ((jsonStation.containsKey("Lat")) && (jsonStation.containsKey("Lon"))) // 'Prague center' and 'Prague periphery' not have position and components list
    {
    String wgs84Latitude = jsonStation.getString("Lat");
    String wgs84Longitude = jsonStation.getString("Lon");

    mapModel.addAirQualityMeasuringStation(new AirQualityMeasuringStation(jsonStation.getString("Code"),
    jsonStation.getString("Name"),
    jsonStation.getString("Owner"), 
    jsonStation.getString("Classif"), 
    Float.valueOf(wgs84Latitude), 
    Float.valueOf(wgs84Longitude), 
    jsonStation.getInt("Ix")));
    }
    }
    }
    }
    } 
    }
    catch (Exception e) 
    {
    e.printStackTrace();
    }
    }

    /**
     * Substitute an empty String, when a null value is encountered.
     * 
     * @param source String
     * @return Original string, or empty string, if parameter is null
     */
    static String nvl(String source)
    {
    return (source == null) ? "" : source;
    }
}

Last line of method map() redirects processing to our View.

Model

Model is represented by three classes:Air quality measuring station

public class AirQualityLegendItem 
{
private final int index;
private final String color;
private final String colorText;
private final String description;

/**
 * Constructor
 * 
 * @param index Air quality index value
 * @param color Background color
 * @param colorText Text color
 * @param description Index description
 */
public AirQualityLegendItem(int index, String color, String colorText, String description)
{
super();
this.index = index;
this.color = color;
this.colorText = colorText;
this.description = description;
}

// getters deleted for shorting
}


public class AirQualityMeasuringStation
{
private final String code;
private final String name;
private final String owner;
private final String classification;
private final float wgs84Latitude;
private final float wgs84Longitude;
private final int index;

/**
 * Constructor
 * 
 * @param code Station unique code
 * @param name Station name
 * @param owner Station owner
 * @param classification Station classification
 * @param wgs84Latitude WGS-84 latitude
 * @param wgs84Longitude WGS-84 longitude
 * @param index Air quality index value
 */
public AirQualityMeasuringStation(String code, String name, String owner, String classification, float wgs84Latitude, float wgs84Longitude, int index) 
{
super();
this.code = code;
this.name = name;
this.owner = owner;
this.classification = classification;
this.wgs84Latitude = wgs84Latitude;
this.wgs84Longitude = wgs84Longitude;
this.index = index;
} 

  // getters deleted for shorting
}


The map model class is anotated as @RequestScoped, and has a name for CDI.

/**
 * The MVC model. Contains data for view:<p/>
 * <code>/WebContent/WEB-INF/jsp/mapView.jsp</code>
 * 
 * @author lvanek
 *
 */
@Named(value="mapModel")
@RequestScoped
public class MapModel 
{
/**
 *  Air quality indexes, key is index value 
 */
private Map<Integer, AirQualityLegendItem> legend = new HashMap<>(8);

/**
 * Air quality measuring stations
 */
private List<AirQualityMeasuringStation> stations = new ArrayList<>();

/**
 * Date of Air quality index actualization
 */
private String actualized;

public String getActualized() {
return actualized;
}

public void setActualized(String actualized) {
this.actualized = actualized;
}

private static final JsonBuilderFactory bf = Json.createBuilderFactory(null);

public Map<Integer, AirQualityLegendItem> getLegend() {
return legend;
}

public void putAirQualiryLegendItem(int index, AirQualityLegendItem item)
{
legend.put(index, item);
}

public AirQualityLegendItem getAirQualiryLegendItem(int index)
{
return legend.get(index);
}

public void addAirQualityMeasuringStation(AirQualityMeasuringStation station)
{
stations.add(station);
}

/**
 * Create Geo JSON with Air quality data. It's requested in mapView.jsp by:<p/>
 * 
 * <code>var geojson = &lt;c:out value="${mapModel.geoJson}" escapeXml="false"/&gt; ;</code>
 * 
 * @return Geo JSON string
 */
public String getGeoJson()
{
JsonObjectBuilder featureCollection = bf.createObjectBuilder().add("type", "FeatureCollection");

JsonArrayBuilder features = Json.createArrayBuilder();

createFeatures(features);

featureCollection.add("features", features);

JsonObject geoJson = featureCollection.build();

return geoJson.toString();
}

/**
 * Populate given GEO JSON features list with Air quality stations data
 * 
 * @param features GEO JSON features
 */
private void createFeatures(JsonArrayBuilder features)
{
/*
 * Sort stations by Air quality index to have worst ones on top layer
 */
Comparator<AirQualityMeasuringStation> comparator = new Comparator<AirQualityMeasuringStation>()
{
public int compare(AirQualityMeasuringStation a1, AirQualityMeasuringStation a2) 
{
return Integer.compare(a1.getIndex(), a2.getIndex());
}
};


Collections.sort(stations, comparator);

/*
 * Traverse stations and create Geo JSON Features
 */
for(AirQualityMeasuringStation station : stations)
{
String title = station.getCode() + " - " + station.getName();

AirQualityLegendItem legendItem = getAirQualiryLegendItem(station.getIndex());

features.add(createFeature(title,
"#" + legendItem.getColorText(),
"#" + legendItem.getColor(), 
station.getWgs84Latitude(), 
station.getWgs84Longitude(),
station.getClassification(),
legendItem.getDescription()));
}
}

/**
 * Create Geo JSON feature
 * 
 * @param title Title for popup
 * @param color Marker text color
 * @param fillColor Marker background color
 * @param wgs84Latitude WGS-84 Latitude
 * @param wgs84Longitude WGS-84 Longitude
 * @param classification Locality classification
 * @param description Air quality index description
 * @return Geo JSON feature
 */
private JsonObjectBuilder createFeature(String title, String color, String fillColor, float wgs84Latitude, float wgs84Longitude, String classification, String description)
{
JsonObjectBuilder feature = Json.createObjectBuilder()
    .add("type", "Feature");

// Feature properties
JsonObjectBuilder properties = Json.createObjectBuilder()
         .add("popupContent", title)
         .add("description", description)
         .add("classification", classification)
         .add("style", Json.createObjectBuilder()
         .add("weight", 1) 
         .add("color", color)
         .add("fillColor", fillColor)
         .add("fillOpacity", 0.8f)
         .add("opacity", 1));

feature.add("properties", properties);

// Feature geometry
JsonObjectBuilder geometry = Json.createObjectBuilder()
         .add("type", "Point")
         .add("coordinates", Json.createArrayBuilder().add(wgs84Longitude).add(wgs84Latitude));

feature.add("geometry", geometry);

return feature;
}
}

Crucial method getGeoJson() will be called from View, and it returns GEO JSON as String. For each station, a Feature is created with this structure:

{
  "type":"Feature",
 "properties":{
      "popupContent":"LFRTA - Frydlant",
      "description":"Index on this station is not determined",
      "classification":"rural",
      "style":{
        "weight":1,
        "color":"#000000",
        "fillColor":"#CFCFCF",
        "fillOpacity":0.800000011920929,
        "opacity":1}
    },
 "geometry":{"type":"Point",
                "coordinates":[15.0698,50.940650]}
}

View

Finally, mapView.jsp is rendered. This is not all of its content, but only the crucial parts.

In head section, Leafleft JavaScript and CSS are included:

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

<head>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
        <link href="./../resources/css/styles.css" rel="stylesheet" type="text/css" />
        <link rel="stylesheet" href="http://cdn.leafletjs.com/leaflet-0.7.5/leaflet.css" />
        <script src="http://cdn.leafletjs.com/leaflet-0.7.5/leaflet.js"></script> 
        <title>Java EE 8 MVC + Leaflet Map demo</title>
 </head>

Then, we need insert map div into page:

 <div id="map" style="height: 800px; position: relative; padding: 0px; margin: 0 auto 0 auto;"></div>


Last but not least, we need some JavaScript code to initialize the map. The crucial part is this line:

var geojson = <c:out value="${mapModel.geoJson}" escapeXml="false"/>;

...which is how MapModel method getGeoJson() called. Then the GEO JSON is processed. For each Feature created, Points are transfered to Circles with appropriate colors.

<script> 
var map = L.map('map').setView([50.0, 15.5], 8);

L.tileLayer('https://api.tiles.mapbox.com/v4/{id}/{z}/{x}/{y}.png?access_token=pk.eyJ1IjoiZmx5aW5nYmlyZCIsImEiOiJjaWd1dXoycWIwYzZ6dmttNWhvdDJlaG5jIn0.I__xI-EzhnkmRI2BB-1SJg', {
maxZoom: 18,
minZoom: 7,
attribution: 'Map data &copy; <a href="http://openstreetmap.org">OpenStreetMap</a> contributors, ' +
'<a href="http://creativecommons.org/licenses/by-sa/2.0/">CC-BY-SA</a>, ' +
'Imagery © <a href="http://mapbox.com">Mapbox</a>',
 id: 'xxx', // Use your own Mapbox id !
 accessToken: 'xxx' // Use your own Mapbox accessToken !
}).addTo(map);


var geojson = <c:out value="${mapModel.geoJson}" escapeXml="false"/>;

function onEachFeature(feature, layer) 
{
var popupContent = "<div style='background-color: " + feature.properties.style.fillColor + ";  padding-left: 2px; padding-right: 2px;'><p style='color: " + feature.properties.style.color + ";'>";

if (feature.properties && feature.properties.popupContent) {
popupContent += "<b> " + feature.properties.popupContent + "</b><br/>";
}

popupContent += "Classification: " + feature.properties.classification + 
"<br/>Air quality: <b>" + feature.properties.description + "</b></p></div>";

layer.bindPopup(popupContent);
}

L.geoJson(geojson, {

style: function (feature) {
return feature.properties && feature.properties.style;
},

onEachFeature: onEachFeature,

pointToLayer: function (feature, latlng) 
{
return L.circleMarker(latlng, {
radius: 8,
fillColor: feature.properties.fillColor,
color: feature.properties.color, 
weight: feature.properties.weight,
opacity: feature.properties.opacity,
fillOpacity: feature.properties.fillOpacity
});
}
}).addTo(map);


</script>

Summary

Creating a simple application with upcoming Java MVC frameworks is very straightforward. I was very pleasantly surprised by the speed of development.

Running application

Running on Glassfish

Glassfish 4.1.1 contains all required technologies, instead of Ozark. I use Eclipse tooling for development (not Maven), so I only put into /WebContent/WEB-INF/lib files ozark-1.0.0-m02.jar and javax.mvc-api-1.0-edr2.jar.

You can find the full source code of the Eclipse project for Glassfish here.

Running on Apache Tomcat 7Libraries on Tomcat

Here, the situation is much more difficult. We need to download CDI implementation WELD (weld-servlet.jar), JAX-RS API Jersey, and incorporate into an application with jstl-1.2_1.jar and javax.json.jar as in the above image.

jersey-cdi*.jar's are 'borrowed' from Glassfish 4.1.1.

Get CDI to work

Create WebContent/META-INF/context.xml:

<?xml version="1.0" encoding="UTF-8"?>
<Context reloadable="true">

    <!-- Default set of monitored resources -->
    <WatchedResource>WEB-INF/web.xml</WatchedResource>

   <Resource name="BeanManager"
      auth="Container"
      type="javax.enterprise.inject.spi.BeanManager"
      factory="org.jboss.weld.resources.ManagerObjectFactory"/>
</Context>

Get Jersey to work

On web.xml, add this content:

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
xmlns="http://java.sun.com/xml/ns/javaee" 
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd" 
id="IskoOzarkApp"
version="3.0">

<display-name>OzarkMapApp</display-name>
<welcome-file-list>
    <welcome-file>index.html</welcome-file>
  </welcome-file-list>

<servlet>
<servlet-name>Jersey REST Service</servlet-name>
<servlet-class>org.glassfish.jersey.servlet.ServletContainer</servlet-class>


<!-- Register JAX-RS Application, if needed. -->
        <init-param>
            <param-name>javax.ws.rs.Application</param-name>
            <param-value>learning.javaee8.mvc.GettingStartedApplication</param-value>
        </init-param>

<!-- Register resources and providers -->
<init-param>
<param-name>jersey.config.server.provider.packages</param-name>
<param-value>idea.isko.mvc.map</param-value>
</init-param>

<!-- Enable Tracing support. -->
        <init-param>
            <param-name>jersey.config.server.tracing</param-name>
            <param-value>ALL</param-value>
        </init-param>

<load-on-startup>1</load-on-startup>
</servlet>

<servlet-mapping>
<servlet-name>Jersey REST Service</servlet-name>
<url-pattern>/rest/*</url-pattern>
</servlet-mapping>

  <resource-env-ref>
  <description>
      Needed to add CDI to Tomcat via Weld 2. It assumes that you have added weld-servlet.jar to WEB-INF/lib and that you
      edited META-INF/context.xml (or the default context.xml) and added a Resource entry for CDI. 

      See http://jsf2.com/using-cdi-and-jsf-2.2-faces-flow-in-tomcat/
    </description>
    <resource-env-ref-name>BeanManager</resource-env-ref-name>
    <resource-env-ref-type>javax.enterprise.inject.spi.BeanManager</resource-env-ref-type>
  </resource-env-ref>

</web-app>

On Tomcat, application path is: http://host:port/OzarkMapApp/rest/map

 

 

 

 

Top