Geospatial SPA using JavaScript only - part 2
Older Article
This article was published 11 years ago. Some information may be outdated or no longer applicable.
In the first part of this series I covered the database backend and JSON document structure. This time, let’s look at the node.js/Express setup.
Quick refresher on what we’re storing. We have JSON documents containing the image filename, extracted geospatial data, and other metadata. These all sit in a collection called image. We also store binary documents (JPEG images) in a collection called binary.
Querying these documents from node.js happens through the node-client-api. (For a detailed explanation, read this article)
Let’s think about what we need. We’ll create endpoints that a frontend service can consume (spoiler: we’ll use Angular).
- One endpoint that lists all data from both collections
- Another endpoint that shows information about a single image
We’re not just building an API, though. We’re also serving HTML pages from Express. So it makes sense to separate routes into ‘app’ routes and ‘api’ routes.
Note that the approach that I am following here is one out of many possible. If you don’t like how I setup my routes feel free to use your own approach.
I created a routes.js file that exports an app and an api object, which get pulled into the main app.js:
var routes = require('./routes').app;
var apiroutes = require('./routes').api;
And the route configuration using Express4:
router.route('/').get(routes.index);
router.route('/api/').get(apiroutes.index);
router.route('/api/:id').get(apiroutes.image);
If you want, you can use a more purposeful library to create the API such as hapi
Express route definitions accept a callback function with request and response parameters. So those callbacks inside .get() are function expressions.
The simplest one is routes.index. It just renders the index.jade file (the application’s homepage):
var appindex = function (req, res) {
res.render('index');
};
Note that both
res.render('index.jade')andres.render('index')yield the same results
Now let’s look at apiroutes.index, which fetches all documents from the database. I like to separate concerns, so the route definition calls another function that does the heavy lifting:
var apiindex = function (req, res) {
selectAll()
.then(function (documents) {
res.json(documents);
})
.catch(function (error) {
console.log('Error: ', error);
});
};
You can probably guess from that .then() call that selectAll() returns a promise:
var selectAll = function selectAll() {
return db.documents
.query(
qb
.where(qb.collection('image'))
.orderBy(qb.sort('filename'))
.slice(0, 300) //return 300 documents "per page" (pagination)
)
.result();
};
selectAll() queries all documents in the image collection and sorts them by filename. That sort requires an index on the filename key. In MarkLogic, that’s an Element Range Index with a String datatype. It creates unique values and enables alphabetical sorting. We need this because we’ll list files using ng-repeat in AngularJS. Without a consistent order, documents would come back randomly and we couldn’t sort inside the ng-repeat loop.
The function returns a promise. Here’s the code that calls it:
var apiindex = function (req, res) {
selectAll()
.then(function (documents) {
res.json(documents);
})
.catch(function (error) {
console.log('Error: ', error);
});
};
If you’ve used Express before, you’ll recognise this as the callback function passed to a routing function. Each one needs req and res parameters.
This anonymous function expression calls selectAll() and handles both promise states (resolve and reject) via .then() and .catch(). If the promise resolves, we return all documents as JSON.
Here’s a screenshot from Postman (my go-to REST testing tool) showing the result:
All JSON documents returned. Now let’s look at fetching a single document.
First, the endpoint:
router.route('/api/:id').get(apiroutes.image);
The implementation in the route:
var selectOne = function selectOne(uri) {
return db.documents.read('/image/' + uri + '.json').result();
};
Notice we’re using .read() instead of .query() here. We’re reading the content of one specific document.
var apiimage = function (req, res) {
var id = req.params.id;
selectOne(id)
.then(function (document) {
if (document.length !== 0) {
res.json(document);
} else {
// this 404 is captured via an AngularJS HTTP Interceptor
res.status(404).end();
}
})
.catch(function (error) {
console.log('Error: ', error);
});
};
We access the URL parameter through req.params using the property name from the route configuration. If a document exists, we send it back as JSON. Otherwise, we return a 404 status. That scenario gets handled later by an AngularJS HTTP Interceptor.
One final piece: we’re only retrieving JSON documents, not the actual images. Let’s create a dedicated endpoint for that:
router.route('/api/imagedata/:id').get(apiroutes.imagedata);
The helper function looks familiar:
var selectImageData = function selectImageData(uri, callback) {
return db.documents.read('/binary/' + uri).result();
};
But when we call it, we need to transform the data coming back from MarkLogic:
var apiimagedata = function (req, res) {
var id = req.params.id;
selectImageData(id)
.then(function (binaryData) {
res.json(new Buffer(binaryData[0].content, 'binary').toString('base64'));
})
.catch(function (error) {
console.log('Error: ', error);
});
};
We return a base64 encoded binary value. The frontend will use this to reconstruct the image.
In the next article, we’ll look at saving data to the database from our API and how relevancy-ranked search works.