Dynamically loading position data with Ionic 4 and Spring Boot

In this blog post we are creating an application that loads location data from a MongoDB database and displays them on a map inside an Ionic 4 application.

Instead of sending all location data at once, the client sends a request to the server with the bounds of the current visible map and the server returns the data points that are located inside this box. Each time the users zooms and moves the map, the client sends a new request to the server with the new bounds of the map. To find the requested locations, the server application takes advantage of the built in geospatial query support in MongoDB.

This application is based on two blog posts from Joshua Morony: Dynamically loading markers with MongoDB: Part1 and Part2.

You should definitely check out Josh Morony's website when you are learning Ionic or want to improve your Ionic skills: https://www.joshmorony.com

He wrote a beginners book about Ionic and offers a course about advanced Ionic topics and he posted a lot of free and interesting blog posts about Ionic.

Compared to Joshua's example I made two changes to the application. Instead of a Node.js server we will create a Spring Boot server with Spring 5 and the reactive web programming model. And we will use OpenStreetMap and Leaflet to display the data points instead of Google Maps.

As example dataset, I chose the earthquakes data collected by the United States Geological Survey (USGS) agency. This dataset is maybe not the best use case for this architecture, because the data points are static. Each time the user zooms and moves the map to the same location, the client loads the same data points multiple times. For static data it might be more useful to load everything at once. But this architecture might be interesting for an application that has to display a lot of data points where the location of each point changes very often. For example, you need to track vehicles or other assets that move around a lot.

For the server I use Spring and Spring Boot with the reactive programming model. Go to https://start.spring.io, select the latest Spring Boot 2 release, enter a group and artifact name, add the dependencies "Reactive Web" and "Reactive MongoDB" and download the zip file. After the download is finished, extract the file and import the project into your favourite IDE.

The data we will download from the USGS site is stored in the CSV (comma-separated values) format. To parse it we will use the Univocity parser library, therefore we add this dependency to our pom.xml

By default, Spring Boot connects to MongoDB on localhost. If your database is running on a different server, add the spring.data.mongodb.host property to the properties file. Also specify the port and username and password if required.

As usual with MongoDB you don't have to create the database beforehand, the database automatically creates it with the first write request.

Next we create an entity class that holds all the properties for an earthquake. Spring Data uses this class to deserialize and serialize the documents from and to MongoDB. We only store a few selected fields from the USGS file. These are all fields the client application will display in a little popup window on the map.

During application start up, Spring Data automatically scans for classes that are annotated with @Document and internally creates a mapping from MongoDB collection to Java class. Because we want to execute geospatial queries in MongoDB, we have to create a special index. Spring Data makes this very convenient by providing the GeoJsonPoint class and the @GeoSpatialIndexed annotation. With this setup Spring Data automatically creates the index in the database during the bootstrap process if it does not already exist.

The default constructor in the Earthquake class is necessary for Spring Data to deserialize the object when it reads the data from the database. The Earthquake(Record record) constructor is used during the initial import where the application converts the CSV record into an entity class and then passes it to Spring Data to save it.

The geo support in MongoDB is not limited to the longitude and latitude coordinate system. You can use any coordinate system, but when you use longitude and latitude, always specify longitude first. The 2d spherical index only recognizes this ordering.

Next we create a Spring Data repository interface and extend the ReactiveSortingRepository interface. This gives our application access to CRUD methods like findAll(), save() and delete(). We add one additional method that returns a collection of earthquakes within a bounding box.

Because we use the reactive programming model where all the code has to run in a nonblocking manner, the method returns a stream of Earthquake instances as Flux. Fortunately for us the underlying MongoDB Java driver already supports asynchronous interactions with the database and Spring Data uses this driver to support the reactive programming model.

The Box class is part of the Spring Data library that describes a box spanning from two given points. We don't have to implement the query logic. Spring Data automatically creates the necessary code for this query derived from the method name. The findBy keyword specifies a search query. The Within keyword results in a geoWithin query and the word Locations matches the name of the field in the Earthquake entity class.

The query that Spring Data sends to MongoDB, when our code calls the method, looks like this.

Next we write the code that downloads and imports the earthquake data. We do this once during the startup of the application. As an enhancement, you could add a scheduled job that periodically imports the latest earthquake data. USGS updates the public available files every 15 minute.

The importer is a Spring managed bean and we inject the ReactiveEarthquakeRepository instance into this class to insert the data into MongoDB.

In the constructor we create an instance of the Spring 5 WebClient and the Univocity CsvParser. The earthquake CSV file contains a header on the first line with the field names therefore we set setHeaderExtractionEnabled(true) and Univocity automatically creates keys in the result with the names of the columns.

You saw the result of this earlier in the Earthquake constructor where we use the name of the column to extract the data (this.id = record.getString("id");)

To run the import we annotate the method with @PostConstruct and Spring automatically calls the method when the application context is set up.

The importer first fetches the data with a HTTP GET request, then calls the parserAllRecords method from the CSVParser, converts each record into an Earthquake instance, deletes all the old data in the database with deleteAll() and saves the newly created entity instances with saveAll().

deleteAll() and saveAll() are methods we get for free from the super interface ReactiveSortingRepository of our repository interface.

Last piece of the server is the controller that handles the request from the client and sends back the earthquakes that are located in the requested area.

The bean is annotated with @RestController which tells Spring to automatically convert the response of all mapping methods to JSON. The server will run during development on localhost:8080 and the Ionic application runs on localhost:8100 so we have to add the @CrossOrigin annotation to allow requests from different origins (CORS).

Like in the importer bean we inject our Spring Data repository. Then we add the method that handles the GET request. The client sends 4 parameters as path variables. Thanks to the @PathVariable annotation, Spring automatically extracts the values from the path and assigns them to the method parameters. The code then creates a Box instance and calls the findByLocationWithin() method. The first parameter of the Box constructor represents the coordinates of the bottom left corner and the second parameter the upper right corner. And always, when you use the longitude/latitude coordinate system with MongoDB, specify the longitude first.

Like the methods in the data repository the methods in this controller have to run in a nonblocking manner and return either a Mono or a Flux.

When it comes to maps, you find a lot of examples that use Google Maps. It's a great service and it's free until a certain amount of requests. But Google Maps is not the only map solution on the Internet. In this example we use the map from OpenStreetMap. OpenStreetMap is a community driven map project, where everybody can help collect data and improve the quality of the map. The data collected by OpenStreetMap is open and free to use for any purpose. You can even run your own map server with it.

Unlike Google Maps OpenStreetMap does not provide an official JavaScript library to display the map in a browser. That's not a problem because there are two popular open source JavaScript libraries we can choose from and are able to read and display the OpenStreetMap data: OpenLayers and Leaflet. Both libraries are provider agnostic, that means they support different map providers as long as you conform to the terms of use and the server sends the data in a format that these libraries understand.

Which of the two libraries you choose, depends on the functionality you need in your application. Leaflet is a lightweight solution (38KB), OpenLayers has more features built in and therefore is bigger. Checkout the documentation and example pages of the two project to see what features they support and choose the library that supports the feature you need in your application.

Both libraries are easy to integrate with Angular. For OpenLayers you can use the ngx-openlayers library and for Leaflet ngx-leaflet. These Angular libraries provide directives that make the integration into your application straightforward.

I chose leaflet and ngx-leaflet for this project because the application only needs to display a basic map and draw some circles on it. Leaflet handles this in a very easy and lightweight way.

The client we create in this project is an Ionic app and is based on the blank starter template

To make the map work we have to add the leaflet directive to an existing DOM element, style the element with a height and provide an initial zoom factor and center with either the leafletOptions or the more specific leafletZoom and leafletCenter input directives.

In our application we set the height and width of the map to 100% to fill the entire screen

In the TypeScript code we import a few objects from the leaflet library. Objects starting with a lowercase character are factory methods that create an object. For instance layerGroup creates a LayerGroup.

The most important configuration is the layers. You have to tell leaflet from where it can download the map data. This is very different to a Google Maps solution because there the JavaScript library and the server are tightly coupled. Leaflet on the other hand supports many map providers. For this example we load the data from the OpenStreetMap (OSM) server. It's important that you additionally specify a zoom factor and a center, leaflet throws an exception when one of them is missing.

In the template we added a listener to the leafletMapReady event (leafletMapReady)="onMapReady($event)". Leaflet emits this event as soon the map is loaded and ready for use.

In the TypeScript code we store the reference to the map in an instance variable and call the loadEarthquakes() method with the bounds of the map. And we install two event listeners for the zoom and move end event. Each time the user zooms and moves the map, leaflet emits these events and each time the application needs to load the earthquakes that are visible in this new area.

As a workaround I had to add the following code. Leaflet would not display the map properly without this code.

We mark each earthquake with a circle on the map. The radius and color change according to the magnitude of the earthquake. Bigger circles and darker colors mean more powerful earthquakes. Instead of adding each circle individually to the map we add the circles to a marker group and add this group to the map. This simplifies the removing of the circles, we only have to remove the group from the map and consequently remove all the contained markers of this group. Worth mentioning here is that the circleMarker factory method expects the location in the order latitude/ longitude which is different to the order MongoDB expects the location.

We also add a little popup window to each circle. You see this information when you click or tap on the circle. The bindPopup method makes this very easy. The method expects a string that contains html code. The info window in this example displays the time, a description of the place and the magnitude of the earthquake.

Now we are ready to test our application. Make sure that the MongoDB instance is up and running. Start the Spring Boot application, either from inside your IDE or from the command line with mvn spring-boot:run. And finally build the Ionic app and open it in a browser with ionic serve.

If there aren't any errors, you should see the earthquakes of the last 30 days. When you move and zoom the map you the see circles dynamically popping in and out depending on the visible area of the map.