The City of Things client library will help you to connect to the City of Things backend. It uses the Web Linking RFC 5988 and interprets the Link header for you. This way you can let the library follow the next/prev/last pages for you.
Installation
npm install cot-lib --save
Usage
Typescript
This library is written in Typescript before being transpiled to Javascript, hence the type information is embedded.
import { CotClient, LocationsClient, SourcesClient, Util } from 'cot-lib';
let client = new CotClient('http://my-cot-backend.eu');
let hash = Util.encodeAsGeohash(31.123456,15.321654);
Javascript
This library can also be used as a regular javascript library.
var CotClient = require('cot-lib').CotClient;
var Util = require('cot-lib').Util;
var client = new CotClient('http://my-cot-backend.eu');
var hash = Util.encodeAsGeohash(31.123456,15.321654);
or
var cot = require('cot-lib');
var client = new cot.CotClient('http://my-cot-backend.eu');
var hash = cot.Util.encodeAsGeohash(31.123456,15.321654);
Creating a CotClient
You start by creating a CotClient object. A CotClient object allows you to request the basic API functions. It needs a reference to the base url of the API.
import { CotClient } from 'cot-lib';
let cotClient = new CotClient('http://my-cot-backend.eu');
Creating a SourcesClient
A SourcesClient object allows you to pre-define all the sourceIds you want to request data from simultaneously. It needs a reference to the base url of the API, but you can get it from a CotClient object too.
import { SourcesClient } from 'cot-lib';
let sourcesClient = new SourcesClient('http://my-cot-backend.eu', 'sourceId1', 'sourceId2', 'sourceId3');
or
import { CotClient, SourcesClient } from 'cot-lib';
let cotClient = new CotClient('http://my-cot-backend.eu');
let sourcesClient = cotClient.createSourcesClient('sourceId1', 'sourceId2', 'sourceId3');
Creating a LocationsClient
A LocationsClient object allows you to pre-define all the geohashes you want to request data from simultaneously. Because of the sheer amount of data, you are required to already filter on the Type you are interested in. It needs a reference to the base url of the API, but you can get it from a CotClient object too.
import { LocationsClient } from 'cot-lib';
let locationsClient = new LocationsClient('http://my-cot-backend.eu');
or
import { CotClient, LocationsClient } from 'cot-lib';
let cotClient = new CotClient('http://my-cot-backend.eu', 'typeId', 'u15kk', 'u15kh', 'u15ke');
let locationsClient = cotClient.createLocationsClient('typeId', 'u15kk', 'u15kh', 'u15ke');
About Observables
This API makes heavy use of ReactiveX Observables. They are a very modern way to do asynchronous communication. When calling subscribe(...)
on an Observable, the Observable is activated and starts its work. It will asynchronously notify its different handlers. The first handler - typically called next - can be notified multiple times, everytime there is new data available, the second one handles errors and the last one is called when the Observable stream has completed its work.
Simple use of Observables
The simplest way to use them is as follows. Given a method getMyData()
that returns an Observable<MyData>
type. This is how you handle it:
Typescript
client.getMyData().subscribe(
nextData => {
},
error => {
},
() => {
}
);
Javascript
client.getMyData().subscribe(
function(nextData) {
},
function(error) {
},
function() {
}
);
There are many more things you can do with Observables, for a more comprehensive guide we refer to the ReactiveX introduction website and the RxJs github page with its excellent Readme.
What about Promises?
An Observable allows you to call the toPromise()
method on it, after which it can be handled as a normal Promise with a then(...)
method. Be aware though that to first argument callback of the then(...)
method will only be called with the very last next update of the observable. So this will not always behave as you would expect.
However we remediated this with two Util methods collectOne(...)
and collectMultiple(...)
that are further explained in the API section. They allow for usage that is more in line with the Promises workflow.
Types
The API uses a couple of Types that it returns.
Type
A Type object describes sources of the same type. It has an id to uniquely identify it. It has a finalizationDelay that determines the delay until the data is considered unchanged and indefinitely cacheable, it has a preferredGranularity that determines the default size of returned TemperalPage brackets for Sources of this Type. At last is has a schema that defines the structure and meaning of the fields in the Source data.
{
"id": "bpost.car",
"finalizationDelay": 0,
"preferredGranularity": "HOURLY",
"schema": {
"co2": "IntNumber (The co2 level.)"
}
}
Source
A Source represents a sensor, actuator or data producer of some sort. It has an id to uniquely identify it. It has a name which is a human readable label for this Source. At last it has a typeRefs field which contains a set of typeId strings that define the Types this Source belongs to.
{
"id": "Car1",
"name": "Bpost Mockup Car 1",
"typeRefs": [
"bpost.car"
]
}
TemporalPage
A TemporalPage object is a bucket (like a page in a pageing mechanism, but spread by time instead of amount). This object contains the parsed Link header values, so they can easily be accessed.
{
"data": {TemporalPageData},
"link": {TemporalPageLink}
}
TemporalPageData
A TemporalPageData object is just the data part of a TemporalPage. It is used when the TemporalPageLink field is no longer relevant in the returned data of a TemporalPage object. (e.g. when requesting data of an interval of multiple pages). (note that next, prev and last fields are optional and not always present).
{
"name": "Car1" or ["Car1", "Car2"],
"columns": ["timestamp","co2","temperature"],
"values": [
[1483201200, 430, 297.15],
[1483201202, 432, 297.13],
[1483201212, 433, 300.01],
[1483201301, 427, 301.14],
[1483201302, 419, 298.06],
[1483201331, 418, 299.48],
[1483201340, 424, 299.82],
]
}
TemporalPageLink
A TemporalPageLink object contains the parsed Link header values relevant for the City of Things client to request follow up requests. It parses the Link header according to the RFC 5988 specification.
{
"self": "/sources/Car1/events/20161231/16?from=1483201200",
"next": "/sources/Car1/events/20161231/17",
"prev": "/sources/Car1/events/20161231/16?to=1483201200",
"last": "/sources/Car1/events/20161231/20?to=1483215300",
}
CotClient API
CotClient(host: string): CotClient
Constructor, call as new CotClient('http://my-cot.backend.eu')
. This will construct a new cotClient to work with.
cotClient.getTypes(): Observable<Type[]>
Returns an Observable containing an array with all available Type objects.
cotClient.getType(typeId: string): Observable<Type>
Returns an Observable containing Type information of the Type with the given typeId.
cotClient.getSourcesOf(typeId: string): Observable<Source[]>
Returns an Observable containing an array of Source objects belonging to the Type with the given typeId.
cotClient.getSources(): Observable<Source[]>
Returns an Observable containing an array of all Source objects currently available.
cotClient.getSource(sourceId: string): Observable<Source>
Returns an Observable containing the Source object with the given sourceId.
cotClient.pollLatestData(sourceId: string, period: number): Observable<TemporalPage>
Continuously poll for the latest data from a Source defined by a given sourcId. The period field is the time in milliseconds in between polling for new values.
cotClient.getDataInterval(sourceId: string, from: number, to: number): Observable<TemporalPageData>
Returns an Observable containing a TemporalPageData object with data from a Source defined by a given sourceId. The TemporalPageData contains the aggregated interval. This method automatically calls the necessary subsequent requests. On every response, the Observable calls its next handler with the newly received data. It is up to the developer to listen for this incoming data and update the already received data.
If you want to wait and only call a handler when all data has come in, use the Util.collectOne(...)
method on this Observable. More information on this method can be found at the end of this API section.
The arguments from and to are UNIX (UTC) timestamps.
cotClient.pollLatestDataIn(geohash: string, typeId: string, period: number): Observable<TemporalPage>
Continuously poll for the latest data in a location, defined by the geohash and belonging to a given typeId. The period field is the time in milliseconds in between polling for new values.
cotClient.getDataIntervalIn(geohash string, typeId: string, from: number, to: number): Observable<TemporalPageData>
Returns an Observable containing a TemporalPageData object with data from Sources belonging to a given typeId, within the given geohash. The TemporalPageData contains the aggregated interval. This method automatically calls the necessary subsequent requests. On every response, the Observable calls its next handler with the newly received data.
It is up to the developer to listen for this incoming data and update the already received data!
If you want to wait and only call a handler when all data has come in, use the Util.collectOne(...)
method on this Observable. More information on this method can be found at the end of this API section.
The arguments from and to are UNIX (UTC) timestamps.
cotClient.createSourcesClient(...sourceIds: string[]): SourcesClient
A convenience method to create a new SourcesClient object, by reusing the host setting from the CotClient.
cotClient.createLocationsClient(...sourceIds: string[]): LocationsClient
A convenience method to create a new LocationsClient object, by reusing the host setting from the CotClient.
SourcesClient API
SourcesClient(host: string, ...sourceIds: string[]): SourcesClient
Constructor, call as new SourcesClient('http://my-cot.backend.eu', 'sourceId1','sourceId2', 'sourceId3')
. This will construct a new sourcesClient to work with.
sourcesClient.pollLatestData(period: number): Observable<TemporalPage>
Poll for the latest data of each Source defined in the client. The period field is the time in milliseconds in between polling for new values. Every update will immediatly be sent through, hence it is mandatory to check the incoming data for the key.
sourcesClient.getDataInterval(sourceId: string, from: number, to: number): Observable<TemporalPageData>
Returns an Observable containing a TemporalPageData object with data from all Source defined by the registered sourceIds of this sourcesClient. The TemporalPageData contains the aggregated interval. This method automatically calls the necessary subsequent requests. On every response, the Observable calls its next handler with the newly received data.
It is up to the developer to listen for this incoming data and update the already received data!
If you want to wait and only call a handler when all data has come in, use the Util.collectMany(...)
method on this Observable. More information on this method can be found at the end of this API section.
The arguments from and to are UNIX (UTC) timestamps.
LocationsClient API
LocationsClient(host: string, ...geohashes: string[]): SourcesClient
Constructor, call as new LocationsClient('http://my-cot.backend.eu', 'geohash1','geohash2', 'geohash3')
. This will construct a new LocationsClient to work with.
locationsClient.pollLatestData(typeId: string period: number): Observable<TemporalPage>
Poll for the latest data from each location, defined in the client by geohashes, filtered by the Type given by the typeId arugment. The period field is the time in milliseconds in between polling for new values. Every update will immediatly be sent through, hence it is mandatory to check the incoming data for the key.
locationsClient.getDataInterval(typeId: string, from: number, to: number): Observable<TemporalPageData>
Returns an Observable containing a TemporalPageData object with data from all locations defined by the registered geohashes of this locationsClient. Because of the sheer amount of data, a typeId must be given to limit the results to Sources from this Type. The TemporalPageData contains the aggregated interval. This method automatically calls the necessary subsequent requests. On every response, the Observable calls its next handler with the newly received data.
It is up to the developer to listen for this incoming data and update the already received data!
If you want to wait and only call a handler when all data has come in, use the Util.collectMany(...)
method on this Observable. More information on this method can be found at the end of this API section.
The arguments from and to are UNIX (UTC) timestamps.
Util API
The Util class contains some static methods that can be helpful when working with the City of Things cilent library.
Util.timestampOf(year: number, month: number, day?: number, hours?: number, minutes?: number, seconds?: number, milliseconds?: number): number
Returns the unix (UTC) timestamp for the given date parameters. The arguments are considered to be in UTC time.
Argument | Optional | Description |
---|
year | no | Four digit number |
month | no | The month in the year (1-12) |
day | yes | The day of the month (1-31) |
hours | yes | The hour of the day (0-23) |
minutes | yes | The minute of the day (0-59) |
seconds | yes | The seconds of the day (0-59) |
milliseconds | yes | The milliseconds of the day (0-999) |
Util.encodeAsGoehash(lat: number, lng: number, precision: number = 9): string
Encodes a latitude-longitude point as a geohash with the given precision.
Argument | Description |
---|
lat | Latitude of point |
lng | Longitude of point |
precision | Character length of geohash (defaults to 9) |
Util.decodeFromGeohash(geohash: string): {"latitude": number, "longitude": number}
Decode a given geohash as a latitude-longitude point.
Util.collectOne(observable: Observable<TemporalPageData>, callback: (data: TemporalPageData) => void, error?: (error: Error) => void): void
This utility method will subscribe to an Observable<TemporalPageData> and collect the results that are sent through on each update of the next handler. Once it is complete, the callback handler will be called with the complete collected data as an argument. An optional error handler is called with the Error object, if one occurs.
Note: Use this in conjunction with CotClient getDataInterval
methods.
import { CotClient, Util } from 'cot-lib';
testMethod() {
let client = new CotClient('http://my-cot-endpoint.eu');
let from = Util.timestampOf(2017, 3, 1, 0, 0, 0, 0);
let to = Util.timestampOf(2017, 4, 1, 0, 0, 0, 0);
let observable = client.getDataIntervalIn('u15kk', 'bpost.cars', from, to);
Util.collectOne(observable, temporalPageData => {
console.log(temporalPageData);
});
}
Util.collectMany(observable: Observable<TemporalPageData>, callback: (data: TemporalPageData[]) => void, error?: (error: Error) => void): void
This utility method will subscribe to an Observable<TemporalPageData> and collect the results that are sent through on each update of the next handler for each endpoint. It will send these updates as an array of TemporalPageData objects. Once it is complete, the callback handler will be called with the complete collected data as an argument, this is again an array of TemporalPageData objects, as many as there are registered sourceIds for a SourcesClient or geohashes for a LocationsClient. An optional error handler is called with the Error object, if one occurs.
Note: Use this in conjunction with SourcesClient and LocationsClient getDataInterval
methods.
import { SourcesClient, Util } from 'cot-lib';
testMethod() {
let sclient = new SourcesClient('http://my-cot-endpoint.eu', 'Car1', 'Car2', 'Car3', 'Car4');
let from = Util.timestampOf(2017, 3, 1, 0, 0, 0, 0);
let to = Util.timestampOf(2017, 4, 1, 0, 0, 0, 0);
let observable = sclient.getDataInterval(from, to);
Util.collectMany(observable, collectedDataArray => {
collectedDataArray.forEach(temporalPageData => {
console.log(temporalPageData);
});
});
}