├── .gitignore ├── README.md ├── bin └── timeseries-server.js ├── config.json ├── example └── data │ ├── rawdata_static.trig │ └── statsdata_static.trig ├── lib ├── CommunicationManager.js ├── Configuration.js ├── DataEventManager.js ├── MultidimensionalInterface.js ├── Utils.js ├── WebSocketClient.js └── interfaces │ ├── GeographicClassification.js │ ├── RawData.js │ └── StatisticalAverage.js ├── package-lock.json ├── package.json └── parking_data.trig /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/* 2 | example/data/RawData/* 3 | example/data/StatData/* 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Time Series server 2 | 3 | The Live Time Series Server is an ongoing implementation that aims on providing a cost efficient interface for 4 | Open Stream data publishing. Through an extensible modular architecture we allow data publishers to define 5 | [multidimensional interfaces](http://ceur-ws.org/Vol-1666/paper-03.pdf) to provide query answering functionalities on top of their data. 6 | 7 | ![Server Architecture](https://linkedtimeseries.github.io/timeseries-demo-paper/media/images/fig1.png) 8 | 9 | Features: 10 | 11 | * Allows to define custom interfaces to publish pre-processed summaries of the data. 12 | * Modular and extensible architecture for implementig new features. 13 | * Keeps and publishes history over HTTP using appropriate caching headers. 14 | * Can expose HTTP and Websocket interfaces for communication with clients. 15 | 16 | A more detailed description can be found on this [demo paper](https://linkedtimeseries.github.io/timeseries-demo-paper/). 17 | 18 | ## RDF Stream 19 | A RDF stream object is a named graph with elements as follows: 20 | ``` 21 | . 22 | <...> <...> <...> . 23 | prov:generatedAtTime "2018-..." . 24 | ``` 25 | As an example we provide a set of around one hour of parking availability [observations](https://github.com/linkedtimeseries/timeseries-server/blob/master/parking_data.trig) 26 | (made every 30 seconds) for the city of Ghent. The examples and implementations we describe next are based on this example data. 27 | 28 | ## Installation 29 | Clone this repository and run `npm install` to install all necessary modules. 30 | 31 | ## Configuration 32 | An example configuration file ([config.json](https://github.com/linkedtimeseries/timeseries-server/blob/master/config.json)) 33 | is provided. This file defines the main communication parameters of the server and as an example, also defines 3 different 34 | multidimensional interfaces (`RawData`, `StatisticalAverage` and `GeographicClassification`). The main parameters are: 35 | ```js 36 | { 37 | "serverUrl": "http://localhost:8080/", // Web server main URL. 38 | "httpPort": 8080, // Web server access port. 39 | "interfaces": [...] // Multidimensional interfaces specification. 40 | } 41 | ``` 42 | 43 | ### RawData 44 | This is the default interface for the server as it takes the received stream updates and exposes them without any modification. 45 | This interface allows also to store historic data as [Linked Data fragments](http://linkeddatafragments.org/). For this example 46 | the data is exposed through HTTP using `/RawData/latest` URL for the most updated data and `/RawData/fragments{?time}` URL 47 | to access historic data. It also exposes an optional Websocket interface to push the latest updates to subscribed clients. 48 | Each interface can define its configuration parameters according to their needs. for this specific implementation these are 49 | the defined parameters: 50 | ```js 51 | { 52 | "name": "RawData", // Interface name. Used to define the HTTP URLs. 53 | "path": "../lib/interface/RawData", // Path to the interface javascript implementation. Used to dynamic module loading. 54 | "websocket": true, // Determines if a Websocket interface will be exposed. 55 | "wsPort": 3001, // Port for Websocket interface. 56 | "fragmentsPath": "./example/data/RawData", // Path to the folder where the historic data will be stored. 57 | "staticTriples": "./example/data/rawdata_static.trig", // Static triples to be aggregated to the data stream. 58 | "maxFileSize": 100000 // Maximun size in Bytes of each historic data fragment. 59 | } 60 | ``` 61 | ### StatisticalAverage 62 | This interface serves as an example of exposing precalculated values from the original data of the stream. On this concrete implementation we expose the arithmetic mean of the parking availability of each defined parking lot in the stream. The data can be accesed on different levels that follow a time-based dimension (i.e. `year`, `month`, `day` and `hour`). Each time a new data update is received, the servers proceeds to re-calculate the arithmetic mean values and updates them on each level. The server also adds metadata using the [Hydra](http://www.hydra-cg.com/spec/latest/core/) and [Multidimensional Interface](http://semweb.datasciencelab.be/ns/multidimensional-interface/#RangeGate) vocabularies to link the different levels together. Next there is a snippet example of the data that can be retrieved at a `month` level using the `TriG` format: 63 | ``` 64 | { 65 | ts:mean "348". 66 | ts:mean "635". 67 | ts:mean "502". 68 | ts:mean "411". 69 | ts:mean "125". 70 | ts:mean "390" 71 | } 72 | "2018-02-28T23:58:20.813Z"; 73 | a mdi:RangeFragment; 74 | mdi:initial "2018-03-01T00:00:00.000Z"; 75 | mdi:final "2018-04-01T00:00:00.000Z"; 76 | mdi:hasRangeGate ; 77 | ts:sampleSize "126"; 78 | a mdi:RangeGate; 79 | hydra:search . 80 | hydra:template "http://localhost:8080/StatisticalAverage/fragment/2018_2019/03_04/{+initial_final}"; 81 | hydra:mapping "http://localhost:8080/StatisticalAverage/fragment/2018_2019/03_04#mapping". 82 | hydra:variable "initial", "final"; 83 | hydra:property mdi:initial, mdi:final. 84 | ``` 85 | ### GeograhicalClassification 86 | Not implemented yet. 87 | ## Test it 88 | To test the server a RDF stream can be piped into the server. We can pipe the example dataset into the server using the [replay-timeseries](https://www.npmjs.com/package/replay-timeseries) 89 | tool, which allows to control the frequency of the updates. Follow the next steps to test the server after installation: 90 | ```bash 91 | $ cd timeseries-server 92 | $ npm install -g replay-timeseries 93 | $ cat parking_data.trig | replay-timeseries -s 10x | node bin/timeseries-server.js -c config.json 94 | ``` 95 | As the original observations were made every 30 seconds, we use `replay-timeseries -s 10x` to replay them every 3 seconds 96 | (10 times faster). This tool also rewrites the `prov:generatedAtTime` value to the current time for testing purposes. 97 | ### HTTP Interfaces 98 | To access the data you can use a polling approach through HTTP as follows: 99 | #### RawData 100 | ```bash 101 | $ curl http://localhost:8080/RawData/latest # Will return the latest stream update. 102 | $ curl -L http://localhost:8080/RawData/fragments # Will redirect to the most recent data fragment. 103 | $ curl -L http://localhost:8080/RawData/fragments?time=2018-03-20T10:15:00.000Z # Will redirect to the fragment containing observations starting on the given time 104 | ``` 105 | Each fragment contains [Hydra](http://www.hydra-cg.com/spec/latest/core/) metadata to link to previous data fragment. 106 | #### StatisticalAverage 107 | Please take into account that the data starts to be calculated from the moment the server is initialized, therefore the dates defined in the test URLs showed next have to be adapted to the moment you run the server. 108 | ```bash 109 | $ curl http://localhost:8080/StatisticalAverage/fragment/2018_2019 # Will return the available calculated averages for the year 2018. 110 | $ curl http://localhost:8080/StatisticalAverage/fragment/2018_2019/03_04 # Will return the available calculated averages for the month 2018/03. 111 | $ curl http://localhost:8080/StatisticalAverage/fragment/2018_2019/03_04/25_26 # Will return the available calculated averages for the day 2018/03/25. 112 | $ curl http://localhost:8080/StatisticalAverage/fragment/2018_2019/03_04/25_26/15_16 # Will return the available calculated averages for the hour 2018/03/25 15:00. 113 | ``` 114 | Each level contains [Hydra](http://www.hydra-cg.com/spec/latest/core/) metadata to link to the upper level and also defines how to query the next available inferior level. 115 | ### Websocket Interface 116 | To access the data through Websockets you can execute the example Websocket [client](https://github.com/linkedtimeseries/timeseries-server/blob/master/lib/WebSocketClient.js) 117 | provided on this implementation as follows: 118 | ```bash 119 | $ cd timeseries-server 120 | $ node lib/WebSocketClient.js 121 | ``` 122 | It subscribes to both the Websocket channels defined by the `RawInterface` and the `StatisticalAverage` interface. It will print on console the data it receives from the server. 123 | ## Future Work 124 | We plan to extend this implementation with the following features: 125 | * Mapping capabilities to work with non-RDF streams. 126 | * Support for [Server-Sent Events](https://html.spec.whatwg.org/multipage/server-sent-events.html#server-sent-events) interfaces. 127 | * A mechanism to weigh Multidimensional Interfaces and determine their cost (cpu, memory, response time, etc) on the server. 128 | * A benchmark evaluation on the different types of communication interfaces to determine the most suitable one for a given data stream. 129 | 130 | ## Authors 131 | Julian Rojas - julianandres.rojasmelendez@ugent.be 132 | Pieter Colpaert - pieter.colpaert@ugent.be 133 | -------------------------------------------------------------------------------- /bin/timeseries-server.js: -------------------------------------------------------------------------------- 1 | const CommunicationManager = require('../lib/CommunicationManager'); 2 | const Configuration = require('../lib/Configuration'); 3 | const DataEventManager = require('../lib/DataEventManager'); 4 | 5 | 6 | try { 7 | // Read config file 8 | let config = Configuration.getConfig(process.argv); 9 | // Init Communication Manager module 10 | let commManager = new CommunicationManager(config); 11 | 12 | // Load multidimensional interfaces 13 | loadInterfaceModules(config.interfaces, commManager); 14 | 15 | //Listen for data on standard input 16 | let stdin = process.openStdin(); 17 | stdin.on('data', chunk => { 18 | // Launch data event towards predifined interfaces through Data Event Manager module 19 | DataEventManager.push('data', chunk); 20 | }); 21 | 22 | // Launch Web server for polling interfaces 23 | let app = commManager.app; 24 | let router = commManager.router; 25 | app.use(router.routes()).use(router.allowedMethods()); 26 | app.listen(config.httpPort); 27 | 28 | } catch (e) { 29 | console.error(e); 30 | process.exit(1); 31 | } 32 | 33 | function loadInterfaceModules(interfaces, commManager) { 34 | for (let i in interfaces) { 35 | let Interface = require(interfaces[i].path); 36 | new Interface(interfaces[i], commManager); 37 | } 38 | } -------------------------------------------------------------------------------- /config.json: -------------------------------------------------------------------------------- 1 | { 2 | "serverUrl": "http://localhost:8080/", 3 | "httpPort": 8080, 4 | "interfaces": [ 5 | { 6 | "name": "RawData", 7 | "path": "../lib/interfaces/RawData", 8 | "websocket": true, 9 | "wsPort": 3001, 10 | "fragmentsPath": "./example/data/RawData", 11 | "staticTriples": "./example/data/rawdata_static.trig", 12 | "maxFileSize": 100000 13 | }, 14 | { 15 | "name": "StatisticalAverage", 16 | "path": "../lib/interfaces/StatisticalAverage", 17 | "websocket": true, 18 | "wsPort": 3002, 19 | "fragmentsPath": "./example/data/StatData", 20 | "staticTriples": "./example/data/statsdata_static.trig" 21 | }, 22 | { 23 | "name": "GeographicClassification", 24 | "path": "../lib/interfaces/GeographicClassification", 25 | "websocket": false 26 | } 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /example/data/rawdata_static.trig: -------------------------------------------------------------------------------- 1 | @prefix datex: . 2 | @prefix schema: . 3 | @prefix dct: . 4 | @prefix geo: . 5 | @prefix owl: . 6 | @prefix rdfs: . 7 | @prefix hydra: . 8 | @prefix void: . 9 | @prefix rdf: . 10 | @prefix foaf: . 11 | @prefix cc: . 12 | @prefix mdi: . 13 | @prefix time: . 14 | @prefix ts: . 15 | 16 | owl:sameAs . 17 | owl:sameAs . 18 | owl:sameAs . 19 | owl:sameAs . 20 | owl:sameAs . 21 | owl:sameAs . 22 | a datex:UrbanParkingSite; 23 | rdfs:label "P07 Sint-Michiels"; 24 | dct:description "Sint-Michiels"; 25 | datex:parkingNumberOfSpaces "450". 26 | a datex:UrbanParkingSite; 27 | rdfs:label "P10 Sint-Pietersplein"; 28 | dct:description "Sint-Pietersplein"; 29 | datex:parkingNumberOfSpaces "708". 30 | a datex:UrbanParkingSite; 31 | rdfs:label "P01 Vrijdagmarkt"; 32 | dct:description "Vrijdagmarkt"; 33 | datex:parkingNumberOfSpaces "647". 34 | a datex:UrbanParkingSite; 35 | rdfs:label "P04 Savaanstraat"; 36 | dct:description "Savaanstraat"; 37 | datex:parkingNumberOfSpaces "540". 38 | a datex:UrbanParkingSite; 39 | rdfs:label "P08 Ramen"; 40 | dct:description "Ramen"; 41 | datex:parkingNumberOfSpaces "266". 42 | a datex:UrbanParkingSite; 43 | rdfs:label "P02 Reep"; 44 | dct:description "Reep"; 45 | datex:parkingNumberOfSpaces "470". -------------------------------------------------------------------------------- /example/data/statsdata_static.trig: -------------------------------------------------------------------------------- 1 | @prefix datex: . 2 | @prefix schema: . 3 | @prefix dct: . 4 | @prefix geo: . 5 | @prefix owl: . 6 | @prefix rdfs: . 7 | @prefix hydra: . 8 | @prefix void: . 9 | @prefix rdf: . 10 | @prefix foaf: . 11 | @prefix cc: . 12 | @prefix mdi: . 13 | @prefix time: . 14 | @prefix ts: . 15 | 16 | owl:sameAs . 17 | owl:sameAs . 18 | owl:sameAs . 19 | owl:sameAs . 20 | owl:sameAs . 21 | owl:sameAs . 22 | a datex:UrbanParkingSite; 23 | rdfs:label "P07 Sint-Michiels"; 24 | dct:description "Sint-Michiels"; 25 | datex:parkingNumberOfSpaces "450". 26 | a datex:UrbanParkingSite; 27 | rdfs:label "P10 Sint-Pietersplein"; 28 | dct:description "Sint-Pietersplein"; 29 | datex:parkingNumberOfSpaces "708". 30 | a datex:UrbanParkingSite; 31 | rdfs:label "P01 Vrijdagmarkt"; 32 | dct:description "Vrijdagmarkt"; 33 | datex:parkingNumberOfSpaces "647". 34 | a datex:UrbanParkingSite; 35 | rdfs:label "P04 Savaanstraat"; 36 | dct:description "Savaanstraat"; 37 | datex:parkingNumberOfSpaces "540". 38 | a datex:UrbanParkingSite; 39 | rdfs:label "P08 Ramen"; 40 | dct:description "Ramen"; 41 | datex:parkingNumberOfSpaces "266". 42 | a datex:UrbanParkingSite; 43 | rdfs:label "P02 Reep"; 44 | dct:description "Reep"; 45 | datex:parkingNumberOfSpaces "470". -------------------------------------------------------------------------------- /lib/CommunicationManager.js: -------------------------------------------------------------------------------- 1 | const Koa = require('koa'); 2 | const Router = require('koa-router'); 3 | const WS = require('ws'); 4 | 5 | class CommunicationManager { 6 | constructor(config) { 7 | this._config = config; 8 | this._app = new Koa(); 9 | this._router = new Router(); 10 | this._webSockets = new Map(); 11 | } 12 | 13 | setupWebSocket(name, port) { 14 | let websocket = new WS.Server({ port: port }); 15 | this.webSockets.set(name, websocket); 16 | } 17 | 18 | pushData(name, data) { 19 | if (this.webSockets.has(name)) { 20 | this.webSockets.get(name).clients.forEach((client) => { 21 | //Check if the connection is open 22 | if (client.readyState === WS.OPEN) { 23 | client.send(data); 24 | } 25 | }); 26 | } else { 27 | throw new Error('There is no WebSocket interface defined for ' + name); 28 | } 29 | } 30 | 31 | get config() { 32 | return this._config; 33 | } 34 | 35 | get app() { 36 | return this._app; 37 | } 38 | 39 | get router() { 40 | return this._router; 41 | } 42 | 43 | get webSockets() { 44 | return this._webSockets; 45 | } 46 | } 47 | 48 | module.exports = CommunicationManager; -------------------------------------------------------------------------------- /lib/Configuration.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | 3 | function getConfig(argv) { 4 | // Get configuration 5 | if (argv[3]) { 6 | try { 7 | let raw = fs.readFileSync(argv[3]); 8 | return JSON.parse(raw); 9 | } catch (e) { 10 | throw e; 11 | } 12 | } else { 13 | throw new Error('Please provide a configuration file using the -c option'); 14 | } 15 | } 16 | 17 | module.exports = { 18 | getConfig: getConfig, 19 | }; 20 | -------------------------------------------------------------------------------- /lib/DataEventManager.js: -------------------------------------------------------------------------------- 1 | var listeners = new Map(); 2 | 3 | class DataEventManager { 4 | 5 | subscribe(label, callback) { 6 | listeners.has(label) || listeners.set(label, []); 7 | listeners.get(label).push(callback); 8 | } 9 | 10 | push(label, ...args) { 11 | let ls = listeners.get(label); 12 | 13 | if (ls && ls.length) { 14 | ls.forEach((callback) => { 15 | callback(...args); 16 | }); 17 | return true; 18 | } 19 | return false; 20 | } 21 | } 22 | 23 | module.exports = new DataEventManager; -------------------------------------------------------------------------------- /lib/MultidimensionalInterface.js: -------------------------------------------------------------------------------- 1 | const DataEventManager = require('./DataEventManager'); 2 | 3 | class MultidimensionalInterface { 4 | 5 | constructor(commMan) { 6 | // Communication Manager object 7 | this._commMan = commMan; 8 | // Latest piece of data obtained 9 | this._latestData = null; 10 | // Subscribe to 'data' events 11 | DataEventManager.subscribe('data', (...data) => this.onData(data)); 12 | 13 | } 14 | 15 | // Abstract methods to be overriden by interface implementations 16 | onData(data) {} 17 | 18 | setupPollInterfaces() {} 19 | 20 | // Websocket interface creator 21 | setupPubsupInterface(name, port) { 22 | this.commMan.setupWebSocket(name, port); 23 | } 24 | 25 | get commMan() { 26 | return this._commMan; 27 | } 28 | 29 | get latestData() { 30 | return this._latestData; 31 | } 32 | 33 | set latestData(data) { 34 | this._latestData = data; 35 | } 36 | } 37 | 38 | module.exports = MultidimensionalInterface; -------------------------------------------------------------------------------- /lib/Utils.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const n3 = require('n3'); 3 | const util = require('util'); 4 | 5 | const readfile = util.promisify(fs.readFile); 6 | const writeFile = util.promisify(fs.writeFile); 7 | const appendFile = util.promisify(fs.appendFile); 8 | 9 | module.exports = new class Utils { 10 | 11 | exists(path) { 12 | return fs.existsSync(path); 13 | } 14 | 15 | createFolder(path) { 16 | if (!fs.existsSync(path)) { 17 | fs.mkdirSync(path); 18 | } 19 | } 20 | 21 | async getFileContent(path) { 22 | return await readfile(path); 23 | } 24 | 25 | async overwriteFile(path, data) { 26 | return await writeFile(path, data); 27 | } 28 | 29 | async appendToFile(path, data) { 30 | return await appendFile(path, data); 31 | } 32 | 33 | getGeneratedAtTimeValue(rdf) { 34 | return new Promise((resolve, reject) => { 35 | n3.Parser().parse(rdf.toString(), (error, triple, prefixes) => { 36 | if (error) { 37 | reject(error); 38 | } 39 | 40 | if (triple && triple.predicate === "http://www.w3.org/ns/prov#generatedAtTime") { 41 | let n3Util = n3.Util; 42 | resolve(new Date(n3Util.getLiteralValue(triple.object))); 43 | } 44 | }); 45 | }); 46 | } 47 | 48 | getAllFragments(path) { 49 | return fs.readdirSync(path); 50 | } 51 | 52 | dateBinarySearch(target, fragments) { 53 | let min = 0; 54 | let max = fragments.length - 1; 55 | let index = null; 56 | 57 | // Checking that target date is contained in the list of fragments. 58 | if (target <= fragments[min]) { 59 | index = min; 60 | } else if (target >= fragments[max]) { 61 | index = max; 62 | } else { 63 | // Perform binary search to find the fragment that contains the target date. 64 | while (index === null) { 65 | // Divide the array in half 66 | let mid = Math.floor((min + max) / 2); 67 | // Target date is in the right half 68 | if (target > fragments[mid]) { 69 | if (target < fragments[mid + 1]) { 70 | index = mid; 71 | } else if (target === fragments[mid + 1]) { 72 | index = mid + 1; 73 | } else { 74 | // Not found yet proceed to divide further this half in 2. 75 | min = mid; 76 | } 77 | // Target date is exactly equals to the middle fragment 78 | } else if (target === fragments[mid]) { 79 | index = mid; 80 | // Target date is on the left half 81 | } else { 82 | if (target >= fragments[mid - 1]) { 83 | index = mid - 1; 84 | } else { 85 | max = mid; 86 | } 87 | } 88 | } 89 | } 90 | 91 | return [new Date(fragments[index]), index]; 92 | } 93 | 94 | getTriplesFromFile(path) { 95 | return new Promise(async (resolve, reject) => { 96 | let parser = n3.Parser(); 97 | let triples = []; 98 | 99 | parser.parse((await readfile(path)).toString(), (err, triple, prefixes) => { 100 | if(triple) { 101 | triples.push(triple); 102 | } else { 103 | resolve([prefixes, triples]); 104 | } 105 | }); 106 | }); 107 | } 108 | 109 | getTriplesFromString(text) { 110 | return new Promise(async (resolve, reject) => { 111 | let parser = n3.Parser(); 112 | let triples = []; 113 | 114 | parser.parse(text, (err, triple, prefixes) => { 115 | if(triple) { 116 | triples.push(triple); 117 | } else { 118 | resolve([prefixes, triples]); 119 | } 120 | }); 121 | }); 122 | } 123 | 124 | formatTriples(format, triples, prefixes) { 125 | return new Promise((resolve, reject) => { 126 | let writer = n3.Writer({ 127 | prefixes: prefixes, 128 | format: format 129 | }); 130 | 131 | writer.addTriples(triples); 132 | 133 | writer.end((err, res) => { 134 | if(err) reject(err); 135 | resolve(res); 136 | }); 137 | }); 138 | } 139 | 140 | getFragmentsCount(path) { 141 | return fs.readdirSync(path).length; 142 | } 143 | 144 | getLiteralValue(literal) { 145 | return n3.Util.getLiteralValue(literal); 146 | } 147 | 148 | getTriplesBySPOG(array, s, p, o, g) { 149 | let temp = array; 150 | 151 | if(s) { 152 | temp = temp.filter(t => t.subject === s); 153 | } 154 | 155 | if(p) { 156 | temp = temp.filter(t => t.predicate === p); 157 | } 158 | 159 | if(o) { 160 | temp = temp.filter(t => t.object === o); 161 | } 162 | 163 | if(g) { 164 | temp = temp.filter(t => t.graph === g); 165 | } 166 | 167 | return temp; 168 | } 169 | } -------------------------------------------------------------------------------- /lib/WebSocketClient.js: -------------------------------------------------------------------------------- 1 | const WebSocket = require('ws'); 2 | 3 | //Create websocket and connect to the server 4 | const raw_ws = new WebSocket('ws://localhost:3001'); 5 | const stats_ws = new WebSocket('ws://localhost:3002'); 6 | 7 | //When the client receive a message, print out the data 8 | raw_ws.on('message', data => { 9 | console.log('Data received from the Raw Interface Websocket:'); 10 | console.log('-----------------------------------------------'); 11 | console.log(data.toString()); 12 | }); 13 | 14 | //When the client receive a message, print out the data 15 | stats_ws.on('message', data => { 16 | console.log('Data received from the Statistical Interface Websocket:'); 17 | console.log('---------------------------------------------------------'); 18 | console.log(data.toString()); 19 | }); 20 | -------------------------------------------------------------------------------- /lib/interfaces/GeographicClassification.js: -------------------------------------------------------------------------------- 1 | const MultidimensionalInterface = require('../../lib/MultidimensionalInterface'); 2 | 3 | class GeographicClassification extends MultidimensionalInterface { 4 | constructor(commMan) { 5 | super(commMan); 6 | } 7 | 8 | onData(data) { 9 | 10 | } 11 | } 12 | 13 | module.exports = GeographicClassification; -------------------------------------------------------------------------------- /lib/interfaces/RawData.js: -------------------------------------------------------------------------------- 1 | const MultidimensionalInterface = require('../../lib/MultidimensionalInterface'); 2 | const Utils = require('../../lib/Utils'); 3 | const md5 = require('md5'); 4 | 5 | class RawData extends MultidimensionalInterface { 6 | 7 | constructor(config, commMan) { 8 | super(commMan); 9 | this._serverUrl = super.commMan.config.serverUrl; 10 | this._name = config.name; 11 | this._websocket = config.websocket; 12 | this._fragmentsPath = config.fragmentsPath; 13 | this._fragmentMaxSize = config.maxFileSize; 14 | this._staticTriples = config.staticTriples; 15 | this._byteCounter = 0; 16 | this._lastFragment = null; 17 | this._lastGat = null; 18 | 19 | // Load HTTP interfaces for this interface 20 | this.setupPollInterfaces(); 21 | 22 | // Load Websocket interface 23 | if (this.websocket) { 24 | super.setupPubsupInterface(this.name, config.wsPort); 25 | } 26 | 27 | // Init storage folder 28 | Utils.createFolder(this.fragmentsPath); 29 | } 30 | 31 | async onData(data) { 32 | this.latestData = data; 33 | this.lastGat = await Utils.getGeneratedAtTimeValue(this.latestData); 34 | 35 | // If applicable push data to subscribed clients through Websocket 36 | if (this.websocket) { 37 | let st = await Utils.getTriplesFromFile(this.staticTriples); 38 | let staticTriples = await Utils.formatTriples('application/trig', st[1], st[0]); 39 | super.commMan.pushData(this.name, staticTriples.concat(data.toString())); 40 | } 41 | 42 | // Store data in files according to config to keep historic data 43 | this.storeData(); 44 | } 45 | 46 | setupPollInterfaces() { 47 | let self = this; 48 | 49 | // HTTP interface to get the latest data update 50 | super.commMan.router.get('/' + this.name + '/latest', async (ctx, next) => { 51 | ctx.response.set({ 'Access-Control-Allow-Origin': '*' }); 52 | if (self.latestData == null) { 53 | ctx.response.status = 404; 54 | ctx.response.body = "No data found"; 55 | } else { 56 | let etag = 'W/"' + md5(this.lastGat) + '"'; 57 | let ifNoneMatchHeader = ctx.request.header['if-none-match']; 58 | let last_modified = this.lastGat.toUTCString(); 59 | //let maxage = self.ldfs.calculateMaxAge(); 60 | //let expires = self.ldfs.calculateExpires(); 61 | 62 | if (ifNoneMatchHeader && ifNoneMatchHeader === etag) { 63 | ctx.response.status = 304; 64 | } else { 65 | ctx.response.set({ 66 | //'Cache-Control': 'public, s-maxage=' + (maxage - 1) + ', max-age=' + maxage + ', must-revalidate', 67 | //'Expires': expires, 68 | 'ETag': etag, 69 | 'Last-Modified': last_modified, 70 | 'Content-Type': 'application/trig' 71 | }); 72 | 73 | let st = await Utils.getTriplesFromFile(this.staticTriples); 74 | let staticTriples = await Utils.formatTriples('application/trig', st[1], st[0]); 75 | ctx.response.body = staticTriples.concat(self.latestData.toString()); 76 | } 77 | } 78 | }); 79 | 80 | // HTTP interface to get a specific fragment of data (historic data) 81 | super.commMan.router.get('/' + this.name + '/fragments', async (ctx, next) => { 82 | let queryTime = new Date(ctx.query.time); 83 | 84 | if (queryTime.toString() === 'Invalid Date') { 85 | // Redirect to now time 86 | ctx.status = 302 87 | ctx.redirect('/' + this.name + '/fragments?time=' + new Date().toISOString()); 88 | return; 89 | } 90 | 91 | let fragments = Utils.getAllFragments(this.fragmentsPath).map(f => new Date(f.substring(0, f.indexOf('.trig'))).getTime()); 92 | let [fragment, index] = Utils.dateBinarySearch(queryTime.getTime(), fragments); 93 | 94 | if (queryTime.getTime() !== fragment.getTime()) { 95 | // Redirect to correct fragment URL 96 | ctx.status = 302 97 | ctx.redirect('/' + this.name + '/fragments?time=' + fragment.toISOString()); 98 | return; 99 | } 100 | 101 | let fc = Utils.getFragmentsCount(this.fragmentsPath); 102 | 103 | let st = await Utils.getTriplesFromFile(this.staticTriples); 104 | let staticTriples = await Utils.formatTriples('application/trig', st[1], st[0]); 105 | let ft = await Utils.getTriplesFromFile(this.fragmentsPath + '/' + fragment.toISOString() + '.trig'); 106 | let fragmentTriples = await Utils.formatTriples('application/trig', ft[1], ft[0]); 107 | let metaData = await this.createMetadata(fragment, index); 108 | 109 | ctx.response.body = staticTriples.concat('\n' + fragmentTriples, '\n' + metaData); 110 | 111 | ctx.response.set({ 112 | 'Access-Control-Allow-Origin': '*', 113 | 'Content-Type': 'application/trig' 114 | }); 115 | 116 | if (index < (fc - 1)) { 117 | // Cache older fragment that won't change over time 118 | ctx.response.set({ 'Cache-Control': 'public, max-age=31536000, inmutable' }); 119 | } else { 120 | // Do not cache current fragment as it will get more data 121 | ctx.response.set({ 'Cache-Control': 'no-cache, no-store, must-revalidate' }); 122 | } 123 | }); 124 | } 125 | 126 | async storeData() { 127 | if (this.byteCounter === 0 || this.byteCounter > this.fragmentMaxSize) { 128 | // Create new fragment 129 | this.lastFragment = this.fragmentsPath + '/' + this.lastGat.toISOString() + '.trig'; 130 | this.byteCounter = 0; 131 | } 132 | 133 | await Utils.appendToFile(this.lastFragment, this.latestData.toString()); 134 | let bytes = Buffer.from(this.latestData.toString()).byteLength; 135 | this.byteCounter += bytes; 136 | } 137 | 138 | async createMetadata(fragment, index) { 139 | let baseUri = this.serverUrl + this.name + '/fragments'; 140 | let subject = baseUri + '?time=' + fragment.toISOString(); 141 | let quads = []; 142 | 143 | quads.push({ 144 | subject: subject, 145 | predicate: 'http://www.w3.org/2000/01/rdf-schema#label', 146 | object: '"Historic and real-time parking data in Ghent"', 147 | graph: '#Metadata' 148 | }); 149 | quads.push({ 150 | subject: subject, 151 | predicate: 'http://www.w3.org/2000/01/rdf-schema#comment', 152 | object: '"This document is a proof of concept mapping using Linked Datex2 by Pieter Colpaert and Julian Rojas"', 153 | graph: '#Metadata' 154 | }); 155 | quads.push({ 156 | subject: subject, 157 | predicate: 'http://xmlns.com/foaf/0.1/homepage', 158 | object: 'https://github.com/smartflanders/ghent-datex2-to-linkeddata', 159 | graph: '#Metadata' 160 | }); 161 | quads.push({ 162 | subject: subject, 163 | predicate: 'http://creativecommons.org/ns#license', 164 | object: 'https://data.stad.gent/algemene-licentie', 165 | graph: '#Metadata' 166 | }); 167 | quads.push({ 168 | subject: subject, 169 | predicate: 'http://www.w3.org/ns/hydra/core#search', 170 | object: subject + '#search', 171 | graph: '#Metadata' 172 | }); 173 | quads.push({ 174 | subject: subject + '#search', 175 | predicate: 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type', 176 | object: 'http://www.w3.org/ns/hydra/core#IriTemplate', 177 | graph: '#Metadata' 178 | }); 179 | quads.push({ 180 | subject: subject + '#search', 181 | predicate: 'http://www.w3.org/ns/hydra/core#template', 182 | object: '"' + baseUri + '{?time}"', 183 | graph: '#Metadata' 184 | }); 185 | quads.push({ 186 | subject: subject + '#search', 187 | predicate: 'http://www.w3.org/ns/hydra/core#variableRepresentation', 188 | object: 'http://www.w3.org/ns/hydra/core#BasicRepresentation', 189 | graph: '#Metadata' 190 | }); 191 | quads.push({ 192 | subject: subject + '#search', 193 | predicate: 'http://www.w3.org/ns/hydra/core#mapping', 194 | object: subject + '#mapping', 195 | graph: '#Metadata' 196 | }); 197 | quads.push({ 198 | subject: subject + '#mapping', 199 | predicate: 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type', 200 | object: 'http://www.w3.org/ns/hydra/core#IriTemplateMapping', 201 | graph: '#Metadata' 202 | }); 203 | quads.push({ 204 | subject: subject + '#mapping', 205 | predicate: 'http://www.w3.org/ns/hydra/core#variable', 206 | object: '"time"', 207 | graph: '#Metadata' 208 | }); 209 | quads.push({ 210 | subject: subject + '#mapping', 211 | predicate: 'http://www.w3.org/ns/hydra/core#required', 212 | object: '"true"^^http://www.w3.org/2001/XMLSchema#boolean', 213 | graph: '#Metadata' 214 | }); 215 | 216 | if (index > 0) { 217 | // Adding hydra:previous link 218 | let fragments = Utils.getAllFragments(this.fragmentsPath); 219 | let previous = fragments[index - 1].substring(0, fragments[index - 1].indexOf('.trig')); 220 | 221 | quads.push({ 222 | subject: subject, 223 | predicate: 'http://www.w3.org/ns/hydra/core#previous', 224 | object: baseUri + '?time=' + previous, 225 | graph: '#Metadata' 226 | }); 227 | } 228 | 229 | return await Utils.formatTriples('application/trig', quads); 230 | } 231 | 232 | get serverUrl() { 233 | return this._serverUrl; 234 | } 235 | 236 | get name() { 237 | return this._name; 238 | } 239 | 240 | get websocket() { 241 | return this._websocket; 242 | } 243 | 244 | get fragmentsPath() { 245 | return this._fragmentsPath; 246 | } 247 | 248 | get fragmentMaxSize() { 249 | return this._fragmentMaxSize; 250 | } 251 | 252 | get staticTriples() { 253 | return this._staticTriples; 254 | } 255 | 256 | get byteCounter() { 257 | return this._byteCounter; 258 | } 259 | 260 | set byteCounter(value) { 261 | this._byteCounter = value; 262 | } 263 | 264 | get lastFragment() { 265 | return this._lastFragment; 266 | } 267 | 268 | set lastFragment(frg) { 269 | this._lastFragment = frg; 270 | } 271 | 272 | get lastGat() { 273 | return this._lastGat; 274 | } 275 | 276 | set lastGat(gat) { 277 | this._lastGat = gat; 278 | } 279 | } 280 | 281 | module.exports = RawData; -------------------------------------------------------------------------------- /lib/interfaces/StatisticalAverage.js: -------------------------------------------------------------------------------- 1 | const MultidimensionalInterface = require('../../lib/MultidimensionalInterface'); 2 | const Utils = require('../../lib/Utils'); 3 | const moment = require('moment'); 4 | 5 | class StatisticalAverage extends MultidimensionalInterface { 6 | constructor(config, commMan) { 7 | super(commMan); 8 | this._serverUrl = super.commMan.config.serverUrl; 9 | this._name = config.name; 10 | this._websocket = config.websocket; 11 | this._fragmentsPath = config.fragmentsPath; 12 | this._staticTriples = config.staticTriples; 13 | this._latestGat = null; 14 | 15 | // Init storage folder 16 | Utils.createFolder(this.fragmentsPath); 17 | 18 | // Load HTTP interfaces for this interface 19 | this.setupPollInterfaces(); 20 | 21 | // Load Websocket interface 22 | if (this.websocket) { 23 | super.setupPubsupInterface(this.name, config.wsPort); 24 | } 25 | } 26 | 27 | async onData(data) { 28 | this.latestData = data; 29 | this.latestGat = moment(await Utils.getGeneratedAtTimeValue(this.latestData)); 30 | let gat = this.latestGat; 31 | gat.utc(); 32 | 33 | let hlevel = await this.handleHourLevel(this.latestData, gat); 34 | let dlevel = await this.handleDayLevel(hlevel, gat); 35 | let mlevel = await this.handleMonthLevel(dlevel, gat); 36 | await this.handleYearLevel(mlevel, gat); 37 | 38 | // If applicable push data to subscribed clients through Websocket 39 | if (this.websocket) { 40 | let st = await Utils.getTriplesFromFile(this.staticTriples); 41 | st[1] = st[1].concat(hlevel[0]); 42 | let rdf = await Utils.formatTriples('application/trig', st[1], st[0]); 43 | super.commMan.pushData(this.name, rdf); 44 | } 45 | } 46 | 47 | setupPollInterfaces() { 48 | let self = this; 49 | 50 | super.commMan.router.get('/' + this.name + '/fragment/:year', async (ctx, next) => { 51 | ctx.response.set({ 'Access-Control-Allow-Origin': '*' }); 52 | let filePath = this.fragmentsPath + '/' + ctx.params.year.split('_')[0] + '.trig'; 53 | await this.handleRequest(ctx, filePath, true); 54 | }); 55 | 56 | super.commMan.router.get('/' + this.name + '/fragment/:year/:month', async (ctx, next) => { 57 | ctx.response.set({ 'Access-Control-Allow-Origin': '*' }); 58 | let filePath = this.fragmentsPath + '/' + ctx.params.year.split('_')[0] + '-' + ctx.params.month.split('_')[0] + '.trig'; 59 | await this.handleRequest(ctx, filePath, true); 60 | }); 61 | 62 | super.commMan.router.get('/' + this.name + '/fragment/:year/:month/:day', async (ctx, next) => { 63 | ctx.response.set({ 'Access-Control-Allow-Origin': '*' }); 64 | let filePath = this.fragmentsPath + '/' + ctx.params.year.split('_')[0] + '-' + ctx.params.month.split('_')[0] + '-' 65 | + ctx.params.day.split('_')[0] + '.trig'; 66 | 67 | await this.handleRequest(ctx, filePath, true); 68 | }); 69 | 70 | super.commMan.router.get('/' + this.name + '/fragment/:year/:month/:day/:hour', async (ctx, next) => { 71 | ctx.response.set({ 'Access-Control-Allow-Origin': '*' }); 72 | let filePath = this.fragmentsPath + '/' + ctx.params.year.split('_')[0] + '-' + ctx.params.month.split('_')[0] + '-' 73 | + ctx.params.day.split('_')[0] + 'T' + ctx.params.hour.split('_')[0] + '.trig'; 74 | 75 | await this.handleRequest(ctx, filePath, false); 76 | }); 77 | } 78 | 79 | async handleRequest(ctx, filePath, metadata) { 80 | if(!Utils.exists(filePath)) { 81 | ctx.response.status = 404; 82 | ctx.response.body = "No data found"; 83 | } else { 84 | let st = await Utils.getTriplesFromFile(this.staticTriples); 85 | let ft = await Utils.getTriplesFromFile(filePath); 86 | 87 | if(metadata) { 88 | let fragmentId = Utils.getTriplesBySPOG(ft[1], null, 'http://www.w3.org/ns/prov#generatedAtTime')[0].subject; 89 | this.addMetadata(fragmentId, ft[1]); 90 | } 91 | 92 | ctx.response.set({'Content-Type': 'text/plain'}); 93 | ctx.response.body = await Utils.formatTriples('application/trig', st[1].concat(ft[1]), st[0]); 94 | } 95 | } 96 | 97 | async handleHourLevel(rawdata, gat) { 98 | let hourPath = this.fragmentsPath + '/' + gat.format('YYYY-MM-DDTHH') + '.trig'; 99 | let data = null; 100 | let values = null; 101 | 102 | if (Utils.exists(hourPath)) { 103 | let newTriples = (await Utils.getTriplesFromString(rawdata.toString()))[1]; 104 | let storedTriples = (await Utils.getTriplesFromFile(hourPath))[1]; 105 | [data, values] = await this.updateHourFragment(newTriples, storedTriples, gat); 106 | await Utils.overwriteFile(hourPath, await Utils.formatTriples('application/trig', data)); 107 | } else { 108 | [data, values] = await this.createHourFragment(gat); 109 | await Utils.appendToFile(hourPath, await Utils.formatTriples('application/trig', data)); 110 | } 111 | 112 | return [data, values]; 113 | } 114 | 115 | async handleDayLevel(hlevel, gat) { 116 | let dayPath = this.fragmentsPath + '/' + gat.format('YYYY-MM-DD') + '.trig'; 117 | let data = null; 118 | let values = null; 119 | 120 | if (Utils.exists(dayPath)) { 121 | let storedTriples = (await Utils.getTriplesFromFile(dayPath))[1]; 122 | [data, values] = await this.updateFragment(hlevel[1], storedTriples, gat); 123 | await Utils.overwriteFile(dayPath, await Utils.formatTriples('application/trig', data)); 124 | } else { 125 | data = this.createDayFragment(hlevel[0], gat); 126 | values = hlevel[1]; 127 | await Utils.appendToFile(dayPath, await Utils.formatTriples('application/trig', data)); 128 | } 129 | 130 | return [data, values]; 131 | } 132 | 133 | async handleMonthLevel(dlevel, gat) { 134 | let monthPath = this.fragmentsPath + '/' + gat.format('YYYY-MM') + '.trig'; 135 | let data = null; 136 | let values = null; 137 | 138 | if (Utils.exists(monthPath)) { 139 | let storedTriples = (await Utils.getTriplesFromFile(monthPath))[1]; 140 | [data, values] = await this.updateFragment(dlevel[1], storedTriples, gat); 141 | await Utils.overwriteFile(monthPath, await Utils.formatTriples('application/trig', data)); 142 | } else { 143 | data = this.createMonthFragment(dlevel[0], gat); 144 | values = dlevel[1]; 145 | await Utils.appendToFile(monthPath, await Utils.formatTriples('application/trig', data)); 146 | } 147 | 148 | return [data, values]; 149 | } 150 | 151 | async handleYearLevel(mlevel, gat) { 152 | let yearPath = this.fragmentsPath + '/' + gat.format('YYYY') + '.trig'; 153 | let data = null; 154 | let values = null; 155 | 156 | if (Utils.exists(yearPath)) { 157 | let storedTriples = (await Utils.getTriplesFromFile(yearPath))[1]; 158 | [data, values] = await this.updateFragment(mlevel[1], storedTriples, gat); 159 | await Utils.overwriteFile(yearPath, await Utils.formatTriples('application/trig', data)); 160 | } else { 161 | data = this.createYearFragment(mlevel[0], gat); 162 | values = mlevel[1]; 163 | await Utils.appendToFile(yearPath, await Utils.formatTriples('application/trig', data)); 164 | } 165 | 166 | return [data, values]; 167 | } 168 | 169 | async createHourFragment(gat) { 170 | let tempDate = moment(gat); 171 | let nextMonth = tempDate.add(1, 'M').format('MM'); 172 | tempDate = moment(gat); 173 | let nextDay = tempDate.add(1, 'd').format('DD'); 174 | tempDate = moment(gat); 175 | let nextHour = tempDate.add(1, 'h').format('HH'); 176 | 177 | let rangeGate = this.serverUrl + this.name + '/fragment/' + gat.year() + '_' + (gat.year() + 1) + '/' 178 | + gat.format('MM') + '_' + nextMonth + '/' + gat.format('DD') + '_' + nextDay + '/'; 179 | let fragmentId = rangeGate + gat.format('HH') + '_' + nextHour; 180 | let quads = (await Utils.getTriplesFromString(this.latestData.toString()))[1]; 181 | let values = new Map(); 182 | 183 | tempDate = moment(gat); 184 | tempDate.minutes(0).seconds(0).milliseconds(0); 185 | 186 | for (let i = 0; i < quads.length; i++) { 187 | if (quads[i].predicate === 'http://vocab.datex.org/terms#parkingNumberOfVacantSpaces') { 188 | quads[i].graph = fragmentId; 189 | quads[i].predicate = 'http://datapiloten.be/vocab/timeseries#mean'; 190 | values.set(quads[i].subject, Utils.getLiteralValue(quads[i].object)); 191 | } 192 | 193 | if (quads[i].predicate === 'http://www.w3.org/ns/prov#generatedAtTime') { 194 | quads[i].subject = fragmentId; 195 | } 196 | } 197 | 198 | quads.push({ 199 | subject: fragmentId, 200 | predicate: 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type', 201 | object: 'http://w3id.org/multidimensional-interface/ontology#RangeFragment' 202 | }); 203 | quads.push({ 204 | subject: fragmentId, 205 | predicate: 'http://w3id.org/multidimensional-interface/ontology#initial', 206 | object: '"' + tempDate.toISOString() + '"' 207 | }); 208 | quads.push({ 209 | subject: fragmentId, 210 | predicate: 'http://w3id.org/multidimensional-interface/ontology#final', 211 | object: '"' + tempDate.add(1, 'h').toISOString() + '"' 212 | }); 213 | quads.push({ 214 | subject: fragmentId, 215 | predicate: 'http://w3id.org/multidimensional-interface/ontology#hasRangeGate', 216 | object: rangeGate 217 | }); 218 | quads.push({ 219 | subject: fragmentId, 220 | predicate: 'http://datapiloten.be/vocab/timeseries#sampleSize', 221 | object: '"1"' 222 | }); 223 | 224 | return [quads, values]; 225 | } 226 | 227 | updateHourFragment(newer, old, gat) { 228 | let newValues = this.getPVFromRawData(newer); 229 | let sampleTriple = Utils.getTriplesBySPOG(old, null, 'http://datapiloten.be/vocab/timeseries#sampleSize')[0]; 230 | let sampleValue = parseInt(Utils.getLiteralValue(sampleTriple.object)); 231 | let values = new Map(); 232 | 233 | for (let i in old) { 234 | if (old[i].predicate === 'http://datapiloten.be/vocab/timeseries#mean') { 235 | old[i].object = '"' + this.calculateMean(parseInt(newValues.get(old[i].subject)), 236 | parseInt(Utils.getLiteralValue(old[i].object)), sampleValue) + '"'; 237 | values.set(old[i].subject, Utils.getLiteralValue(old[i].object)); 238 | } 239 | 240 | if (old[i].predicate === 'http://www.w3.org/ns/prov#generatedAtTime') { 241 | old[i].object = '"' + gat.toISOString() + '"'; 242 | } 243 | 244 | if (old[i].predicate === 'http://datapiloten.be/vocab/timeseries#sampleSize') { 245 | old[i].object = '"' + (sampleValue + 1) + '"'; 246 | } 247 | } 248 | 249 | return [old, values]; 250 | } 251 | 252 | createDayFragment(hlevel, gat) { 253 | let tempDate = moment(gat); 254 | let nextMonth = tempDate.add(1, 'M').format('MM'); 255 | tempDate = moment(gat); 256 | let nextDay = tempDate.add(1, 'd').format('DD'); 257 | 258 | let rangeGate = this.serverUrl + this.name + '/fragment/' + gat.year() + '_' + (gat.year() + 1) + '/' 259 | + gat.format('MM') + '_' + nextMonth + '/'; 260 | let fragmentId = rangeGate + gat.format('DD') + '_' + nextDay; 261 | 262 | tempDate = moment(gat); 263 | tempDate.hours(0).minutes(0).seconds(0).milliseconds(0); 264 | 265 | for (let i = 0; i < hlevel.length; i++) { 266 | if (hlevel[i].graph) { 267 | hlevel[i].graph = fragmentId; 268 | } else { 269 | hlevel[i].subject = fragmentId; 270 | } 271 | 272 | if (hlevel[i].predicate === 'http://w3id.org/multidimensional-interface/ontology#initial') { 273 | hlevel[i].object = '"' + tempDate.toISOString() + '"'; 274 | } 275 | 276 | if (hlevel[i].predicate === 'http://w3id.org/multidimensional-interface/ontology#final') { 277 | hlevel[i].object = '"' + tempDate.add(1, 'd').toISOString() + '"'; 278 | } 279 | 280 | if (hlevel[i].predicate === 'http://w3id.org/multidimensional-interface/ontology#hasRangeGate') { 281 | hlevel[i].object = rangeGate; 282 | } 283 | 284 | if (hlevel[i].predicate === 'http://datapiloten.be/vocab/timeseries#sampleSize') { 285 | hlevel[i].object = '"1"'; 286 | } 287 | } 288 | 289 | //this.addMetadata(fragmentId, hlevel); 290 | 291 | return hlevel; 292 | } 293 | 294 | createMonthFragment(dlevel, gat) { 295 | let tempDate = moment(gat); 296 | let nextMonth = tempDate.add(1, 'M').format('MM'); 297 | 298 | let rangeGate = this.serverUrl + this.name + '/fragment/' + gat.year() + '_' + (gat.year() + 1) + '/'; 299 | let fragmentId = rangeGate + gat.format('MM') + '_' + nextMonth; 300 | 301 | tempDate = moment(gat); 302 | tempDate.date(1).hours(0).minutes(0).seconds(0).milliseconds(0); 303 | 304 | for (let i = 0; i < dlevel.length; i++) { 305 | if (dlevel[i].graph) { 306 | dlevel[i].graph = fragmentId; 307 | } else { 308 | dlevel[i].subject = fragmentId; 309 | } 310 | 311 | if (dlevel[i].predicate === 'http://w3id.org/multidimensional-interface/ontology#initial') { 312 | dlevel[i].object = '"' + tempDate.toISOString() + '"'; 313 | } 314 | 315 | if (dlevel[i].predicate === 'http://w3id.org/multidimensional-interface/ontology#final') { 316 | dlevel[i].object = '"' + tempDate.add(1, 'M').toISOString() + '"'; 317 | } 318 | 319 | if (dlevel[i].predicate === 'http://w3id.org/multidimensional-interface/ontology#hasRangeGate') { 320 | dlevel[i].object = rangeGate; 321 | } 322 | 323 | if (dlevel[i].predicate === 'http://datapiloten.be/vocab/timeseries#sampleSize') { 324 | dlevel[i].object = '"1"'; 325 | } 326 | } 327 | 328 | //this.addMetadata(fragmentId, dlevel); 329 | 330 | return dlevel; 331 | } 332 | 333 | createYearFragment(mlevel, gat) { 334 | let tempDate = moment(gat); 335 | let fragmentId = this.serverUrl + this.name + '/fragment/' + gat.year() + '_' + (gat.year() + 1); 336 | 337 | tempDate.month(0).date(1).hours(0).minutes(0).seconds(0).milliseconds(0); 338 | 339 | for (let i = 0; i < mlevel.length; i++) { 340 | if (mlevel[i].graph) { 341 | mlevel[i].graph = fragmentId; 342 | } else { 343 | mlevel[i].subject = fragmentId; 344 | } 345 | 346 | if (mlevel[i].predicate === 'http://w3id.org/multidimensional-interface/ontology#initial') { 347 | mlevel[i].object = '"' + tempDate.toISOString() + '"'; 348 | } 349 | 350 | if (mlevel[i].predicate === 'http://w3id.org/multidimensional-interface/ontology#final') { 351 | mlevel[i].object = '"' + tempDate.add(1, 'y').toISOString() + '"'; 352 | } 353 | 354 | if (mlevel[i].predicate === 'http://datapiloten.be/vocab/timeseries#sampleSize') { 355 | mlevel[i].object = '"1"'; 356 | } 357 | } 358 | 359 | mlevel = mlevel.filter(m => m.predicate !== 'http://w3id.org/multidimensional-interface/ontology#hasRangeGate'); 360 | 361 | //this.addMetadata(fragmentId, mlevel); 362 | 363 | return mlevel; 364 | } 365 | 366 | updateFragment(newValues, old, gat) { 367 | let sampleTriple = Utils.getTriplesBySPOG(old, null, 'http://datapiloten.be/vocab/timeseries#sampleSize')[0]; 368 | let sampleValue = parseInt(Utils.getLiteralValue(sampleTriple.object)); 369 | 370 | for (let i in old) { 371 | if (old[i].predicate === 'http://datapiloten.be/vocab/timeseries#mean') { 372 | old[i].object = '"' + this.calculateMean(parseInt(newValues.get(old[i].subject)), 373 | parseInt(Utils.getLiteralValue(old[i].object)), sampleValue) + '"'; 374 | newValues.set(old[i].subject, Utils.getLiteralValue(old[i].object)); 375 | } 376 | 377 | if (old[i].predicate === 'http://www.w3.org/ns/prov#generatedAtTime') { 378 | old[i].object = '"' + gat.toISOString() + '"'; 379 | } 380 | 381 | if (old[i].predicate === 'http://datapiloten.be/vocab/timeseries#sampleSize') { 382 | old[i].object = '"' + (sampleValue + 1) + '"'; 383 | } 384 | } 385 | 386 | return [old, newValues]; 387 | } 388 | 389 | addMetadata(fragmentId, level) { 390 | level.push({ 391 | subject: fragmentId, 392 | predicate: 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type', 393 | object: 'http://w3id.org/multidimensional-interface/ontology#RangeGate' 394 | }); 395 | level.push({ 396 | subject: fragmentId, 397 | predicate: 'http://www.w3.org/ns/hydra/core#search', 398 | object: fragmentId + '#search' 399 | }); 400 | level.push({ 401 | subject: fragmentId + '#search', 402 | predicate: 'http://www.w3.org/ns/hydra/core#template', 403 | object: '"' + fragmentId + '/{+initial_final}' + '"' 404 | }); 405 | level.push({ 406 | subject: fragmentId + '#search', 407 | predicate: 'http://www.w3.org/ns/hydra/core#mapping', 408 | object: '"' + fragmentId + '#mapping' + '"' 409 | }); 410 | level.push({ 411 | subject: fragmentId + '#mapping', 412 | predicate: 'http://www.w3.org/ns/hydra/core#variable', 413 | object: '"initial"' 414 | }); 415 | level.push({ 416 | subject: fragmentId + '#mapping', 417 | predicate: 'http://www.w3.org/ns/hydra/core#variable', 418 | object: '"final"' 419 | }); 420 | level.push({ 421 | subject: fragmentId + '#mapping', 422 | predicate: 'http://www.w3.org/ns/hydra/core#property', 423 | object: 'http://w3id.org/multidimensional-interface/ontology#initial' 424 | }); 425 | level.push({ 426 | subject: fragmentId + '#mapping', 427 | predicate: 'http://www.w3.org/ns/hydra/core#property', 428 | object: 'http://w3id.org/multidimensional-interface/ontology#final' 429 | }); 430 | } 431 | 432 | getPVFromRawData(triples) { 433 | let res = new Map(); 434 | for (let i in triples) { 435 | if (triples[i].predicate === 'http://vocab.datex.org/terms#parkingNumberOfVacantSpaces') { 436 | res.set(triples[i].subject, Utils.getLiteralValue(triples[i].object)); 437 | } 438 | } 439 | return res; 440 | } 441 | 442 | calculateMean(n, aggregate, sample) { 443 | return Math.floor(((aggregate * sample) + n) / (sample + 1)); 444 | } 445 | 446 | get serverUrl() { 447 | return this._serverUrl; 448 | } 449 | 450 | get name() { 451 | return this._name; 452 | } 453 | 454 | get websocket() { 455 | return this._websocket; 456 | } 457 | 458 | get fragmentsPath() { 459 | return this._fragmentsPath; 460 | } 461 | 462 | get staticTriples() { 463 | return this._staticTriples; 464 | } 465 | 466 | get latestGat() { 467 | return this._latestGat; 468 | } 469 | 470 | set latestGat(date) { 471 | this._latestGat = date; 472 | } 473 | } 474 | 475 | module.exports = StatisticalAverage; -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "timeseries-server", 3 | "version": "0.1.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "accepts": { 8 | "version": "1.3.5", 9 | "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.5.tgz", 10 | "integrity": "sha1-63d99gEXI6OxTopywIBcjoZ0a9I=", 11 | "requires": { 12 | "mime-types": "2.1.18", 13 | "negotiator": "0.6.1" 14 | } 15 | }, 16 | "any-promise": { 17 | "version": "1.3.0", 18 | "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", 19 | "integrity": "sha1-q8av7tzqUugJzcA3au0845Y10X8=" 20 | }, 21 | "async-limiter": { 22 | "version": "1.0.0", 23 | "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.0.tgz", 24 | "integrity": "sha512-jp/uFnooOiO+L211eZOoSyzpOITMXx1rBITauYykG3BRYPu8h0UcxsPNB04RR5vo4Tyz3+ay17tR6JVf9qzYWg==" 25 | }, 26 | "charenc": { 27 | "version": "0.0.2", 28 | "resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz", 29 | "integrity": "sha1-wKHS86cJLgN3S/qD8UwPxXkKhmc=" 30 | }, 31 | "co": { 32 | "version": "4.6.0", 33 | "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", 34 | "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=" 35 | }, 36 | "content-disposition": { 37 | "version": "0.5.2", 38 | "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz", 39 | "integrity": "sha1-DPaLud318r55YcOoUXjLhdunjLQ=" 40 | }, 41 | "content-type": { 42 | "version": "1.0.4", 43 | "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", 44 | "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==" 45 | }, 46 | "cookies": { 47 | "version": "0.7.1", 48 | "resolved": "https://registry.npmjs.org/cookies/-/cookies-0.7.1.tgz", 49 | "integrity": "sha1-fIphX1SBxhq58WyDNzG8uPZjuZs=", 50 | "requires": { 51 | "depd": "1.1.2", 52 | "keygrip": "1.0.2" 53 | } 54 | }, 55 | "crypt": { 56 | "version": "0.0.2", 57 | "resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz", 58 | "integrity": "sha1-iNf/fsDfuG9xPch7u0LQRNPmxBs=" 59 | }, 60 | "debug": { 61 | "version": "3.1.0", 62 | "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", 63 | "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", 64 | "requires": { 65 | "ms": "2.0.0" 66 | } 67 | }, 68 | "deep-equal": { 69 | "version": "1.0.1", 70 | "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.0.1.tgz", 71 | "integrity": "sha1-9dJgKStmDghO/0zbyfCK0yR0SLU=" 72 | }, 73 | "delegates": { 74 | "version": "1.0.0", 75 | "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", 76 | "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=" 77 | }, 78 | "depd": { 79 | "version": "1.1.2", 80 | "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", 81 | "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=" 82 | }, 83 | "destroy": { 84 | "version": "1.0.4", 85 | "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", 86 | "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" 87 | }, 88 | "ee-first": { 89 | "version": "1.1.1", 90 | "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", 91 | "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" 92 | }, 93 | "error-inject": { 94 | "version": "1.0.0", 95 | "resolved": "https://registry.npmjs.org/error-inject/-/error-inject-1.0.0.tgz", 96 | "integrity": "sha1-4rPZG1Su1nLzCdlQ0VSFD6EdTzc=" 97 | }, 98 | "escape-html": { 99 | "version": "1.0.3", 100 | "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", 101 | "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" 102 | }, 103 | "fresh": { 104 | "version": "0.5.2", 105 | "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", 106 | "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=" 107 | }, 108 | "http-assert": { 109 | "version": "1.3.0", 110 | "resolved": "https://registry.npmjs.org/http-assert/-/http-assert-1.3.0.tgz", 111 | "integrity": "sha1-oxpc+IyHPsu1eWkH1NbxMujAHko=", 112 | "requires": { 113 | "deep-equal": "1.0.1", 114 | "http-errors": "1.6.2" 115 | } 116 | }, 117 | "http-errors": { 118 | "version": "1.6.2", 119 | "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.2.tgz", 120 | "integrity": "sha1-CgAsyFcHGSp+eUbO7cERVfYOxzY=", 121 | "requires": { 122 | "depd": "1.1.1", 123 | "inherits": "2.0.3", 124 | "setprototypeof": "1.0.3", 125 | "statuses": "1.4.0" 126 | }, 127 | "dependencies": { 128 | "depd": { 129 | "version": "1.1.1", 130 | "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.1.tgz", 131 | "integrity": "sha1-V4O04cRZ8G+lyif5kfPQbnoxA1k=" 132 | } 133 | } 134 | }, 135 | "inherits": { 136 | "version": "2.0.3", 137 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", 138 | "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" 139 | }, 140 | "is-buffer": { 141 | "version": "1.1.6", 142 | "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", 143 | "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==" 144 | }, 145 | "is-generator-function": { 146 | "version": "1.0.7", 147 | "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.7.tgz", 148 | "integrity": "sha512-YZc5EwyO4f2kWCax7oegfuSr9mFz1ZvieNYBEjmukLxgXfBUbxAWGVF7GZf0zidYtoBl3WvC07YK0wT76a+Rtw==" 149 | }, 150 | "isarray": { 151 | "version": "0.0.1", 152 | "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", 153 | "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" 154 | }, 155 | "keygrip": { 156 | "version": "1.0.2", 157 | "resolved": "https://registry.npmjs.org/keygrip/-/keygrip-1.0.2.tgz", 158 | "integrity": "sha1-rTKXxVcGneqLz+ek+kkbdcXd65E=" 159 | }, 160 | "koa": { 161 | "version": "2.5.0", 162 | "resolved": "https://registry.npmjs.org/koa/-/koa-2.5.0.tgz", 163 | "integrity": "sha512-UkrbMW2mRNfoW/4I20knJEjtPAWCV3Iw6f4XdnPWjHsCN8iTeSh0eSutrYdL0fGF/G9on2eQ30EEQif0MarGJA==", 164 | "requires": { 165 | "accepts": "1.3.5", 166 | "content-disposition": "0.5.2", 167 | "content-type": "1.0.4", 168 | "cookies": "0.7.1", 169 | "debug": "3.1.0", 170 | "delegates": "1.0.0", 171 | "depd": "1.1.2", 172 | "destroy": "1.0.4", 173 | "error-inject": "1.0.0", 174 | "escape-html": "1.0.3", 175 | "fresh": "0.5.2", 176 | "http-assert": "1.3.0", 177 | "http-errors": "1.6.2", 178 | "is-generator-function": "1.0.7", 179 | "koa-compose": "4.0.0", 180 | "koa-convert": "1.2.0", 181 | "koa-is-json": "1.0.0", 182 | "mime-types": "2.1.18", 183 | "on-finished": "2.3.0", 184 | "only": "0.0.2", 185 | "parseurl": "1.3.2", 186 | "statuses": "1.4.0", 187 | "type-is": "1.6.16", 188 | "vary": "1.1.2" 189 | } 190 | }, 191 | "koa-compose": { 192 | "version": "4.0.0", 193 | "resolved": "https://registry.npmjs.org/koa-compose/-/koa-compose-4.0.0.tgz", 194 | "integrity": "sha1-KAClE9nDYe8NY4UrA45Pby1adzw=" 195 | }, 196 | "koa-convert": { 197 | "version": "1.2.0", 198 | "resolved": "https://registry.npmjs.org/koa-convert/-/koa-convert-1.2.0.tgz", 199 | "integrity": "sha1-2kCHXfSd4FOQmNFwC1CCDOvNIdA=", 200 | "requires": { 201 | "co": "4.6.0", 202 | "koa-compose": "3.2.1" 203 | }, 204 | "dependencies": { 205 | "koa-compose": { 206 | "version": "3.2.1", 207 | "resolved": "https://registry.npmjs.org/koa-compose/-/koa-compose-3.2.1.tgz", 208 | "integrity": "sha1-qFzLQLfZhtjlo0Wzoazo6rz1Tec=", 209 | "requires": { 210 | "any-promise": "1.3.0" 211 | } 212 | } 213 | } 214 | }, 215 | "koa-is-json": { 216 | "version": "1.0.0", 217 | "resolved": "https://registry.npmjs.org/koa-is-json/-/koa-is-json-1.0.0.tgz", 218 | "integrity": "sha1-JzwH7c3Ljfaiwat9We52SRRR7BQ=" 219 | }, 220 | "koa-router": { 221 | "version": "7.4.0", 222 | "resolved": "https://registry.npmjs.org/koa-router/-/koa-router-7.4.0.tgz", 223 | "integrity": "sha512-IWhaDXeAnfDBEpWS6hkGdZ1ablgr6Q6pGdXCyK38RbzuH4LkUOpPqPw+3f8l8aTDrQmBQ7xJc0bs2yV4dzcO+g==", 224 | "requires": { 225 | "debug": "3.1.0", 226 | "http-errors": "1.6.2", 227 | "koa-compose": "3.2.1", 228 | "methods": "1.1.2", 229 | "path-to-regexp": "1.7.0", 230 | "urijs": "1.19.1" 231 | }, 232 | "dependencies": { 233 | "koa-compose": { 234 | "version": "3.2.1", 235 | "resolved": "https://registry.npmjs.org/koa-compose/-/koa-compose-3.2.1.tgz", 236 | "integrity": "sha1-qFzLQLfZhtjlo0Wzoazo6rz1Tec=", 237 | "requires": { 238 | "any-promise": "1.3.0" 239 | } 240 | } 241 | } 242 | }, 243 | "md5": { 244 | "version": "2.2.1", 245 | "resolved": "https://registry.npmjs.org/md5/-/md5-2.2.1.tgz", 246 | "integrity": "sha1-U6s41f48iJG6RlMp6iP6wFQBJvk=", 247 | "requires": { 248 | "charenc": "0.0.2", 249 | "crypt": "0.0.2", 250 | "is-buffer": "1.1.6" 251 | } 252 | }, 253 | "media-typer": { 254 | "version": "0.3.0", 255 | "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", 256 | "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" 257 | }, 258 | "methods": { 259 | "version": "1.1.2", 260 | "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", 261 | "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=" 262 | }, 263 | "mime-db": { 264 | "version": "1.33.0", 265 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.33.0.tgz", 266 | "integrity": "sha512-BHJ/EKruNIqJf/QahvxwQZXKygOQ256myeN/Ew+THcAa5q+PjyTTMMeNQC4DZw5AwfvelsUrA6B67NKMqXDbzQ==" 267 | }, 268 | "mime-types": { 269 | "version": "2.1.18", 270 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.18.tgz", 271 | "integrity": "sha512-lc/aahn+t4/SWV/qcmumYjymLsWfN3ELhpmVuUFjgsORruuZPVSwAQryq+HHGvO/SI2KVX26bx+En+zhM8g8hQ==", 272 | "requires": { 273 | "mime-db": "1.33.0" 274 | } 275 | }, 276 | "moment": { 277 | "version": "2.21.0", 278 | "resolved": "https://registry.npmjs.org/moment/-/moment-2.21.0.tgz", 279 | "integrity": "sha512-TCZ36BjURTeFTM/CwRcViQlfkMvL1/vFISuNLO5GkcVm1+QHfbSiNqZuWeMFjj1/3+uAjXswgRk30j1kkLYJBQ==" 280 | }, 281 | "ms": { 282 | "version": "2.0.0", 283 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", 284 | "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" 285 | }, 286 | "n3": { 287 | "version": "0.11.2", 288 | "resolved": "https://registry.npmjs.org/n3/-/n3-0.11.2.tgz", 289 | "integrity": "sha512-ICSiOmFLbZ4gI35+4H3e2vYGHDC944WZkCa1iVNRAx/mRZESEevQNFhfHaui/lhqynoZYvBVDNjM/2Tfd3TICQ==" 290 | }, 291 | "negotiator": { 292 | "version": "0.6.1", 293 | "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.1.tgz", 294 | "integrity": "sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk=" 295 | }, 296 | "on-finished": { 297 | "version": "2.3.0", 298 | "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", 299 | "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", 300 | "requires": { 301 | "ee-first": "1.1.1" 302 | } 303 | }, 304 | "only": { 305 | "version": "0.0.2", 306 | "resolved": "https://registry.npmjs.org/only/-/only-0.0.2.tgz", 307 | "integrity": "sha1-Kv3oTQPlC5qO3EROMGEKcCle37Q=" 308 | }, 309 | "parseurl": { 310 | "version": "1.3.2", 311 | "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.2.tgz", 312 | "integrity": "sha1-/CidTtiZMRlGDBViUyYs3I3mW/M=" 313 | }, 314 | "path-to-regexp": { 315 | "version": "1.7.0", 316 | "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.7.0.tgz", 317 | "integrity": "sha1-Wf3g9DW62suhA6hOnTvGTpa5k30=", 318 | "requires": { 319 | "isarray": "0.0.1" 320 | } 321 | }, 322 | "safe-buffer": { 323 | "version": "5.1.1", 324 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz", 325 | "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==" 326 | }, 327 | "setprototypeof": { 328 | "version": "1.0.3", 329 | "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.0.3.tgz", 330 | "integrity": "sha1-ZlZ+NwQ+608E2RvWWMDL77VbjgQ=" 331 | }, 332 | "statuses": { 333 | "version": "1.4.0", 334 | "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz", 335 | "integrity": "sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew==" 336 | }, 337 | "type-is": { 338 | "version": "1.6.16", 339 | "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.16.tgz", 340 | "integrity": "sha512-HRkVv/5qY2G6I8iab9cI7v1bOIdhm94dVjQCPFElW9W+3GeDOSHmy2EBYe4VTApuzolPcmgFTN3ftVJRKR2J9Q==", 341 | "requires": { 342 | "media-typer": "0.3.0", 343 | "mime-types": "2.1.18" 344 | } 345 | }, 346 | "ultron": { 347 | "version": "1.1.1", 348 | "resolved": "https://registry.npmjs.org/ultron/-/ultron-1.1.1.tgz", 349 | "integrity": "sha512-UIEXBNeYmKptWH6z8ZnqTeS8fV74zG0/eRU9VGkpzz+LIJNs8W/zM/L+7ctCkRrgbNnnR0xxw4bKOr0cW0N0Og==" 350 | }, 351 | "urijs": { 352 | "version": "1.19.1", 353 | "resolved": "https://registry.npmjs.org/urijs/-/urijs-1.19.1.tgz", 354 | "integrity": "sha512-xVrGVi94ueCJNrBSTjWqjvtgvl3cyOTThp2zaMaFNGp3F542TR6sM3f2o8RqZl+AwteClSVmoCyt0ka4RjQOQg==" 355 | }, 356 | "vary": { 357 | "version": "1.1.2", 358 | "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", 359 | "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=" 360 | }, 361 | "ws": { 362 | "version": "3.3.3", 363 | "resolved": "https://registry.npmjs.org/ws/-/ws-3.3.3.tgz", 364 | "integrity": "sha512-nnWLa/NwZSt4KQJu51MYlCcSQ5g7INpOrOMt4XV8j4dqTXdmlUmSHQ8/oLC069ckre0fRsgfvsKwbTdtKLCDkA==", 365 | "requires": { 366 | "async-limiter": "1.0.0", 367 | "safe-buffer": "5.1.1", 368 | "ultron": "1.1.1" 369 | } 370 | } 371 | } 372 | } 373 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "timeseries-server", 3 | "version": "0.1.0", 4 | "description": "A server to host linked time series data as linked data fragments", 5 | "main": "bin/timeseries-server.js", 6 | "dependencies": { 7 | "koa": "^2.4.1", 8 | "koa-router": "^7.2.1", 9 | "md5": "^2.2.1", 10 | "moment": "^2.21.0", 11 | "n3": "^0.11.2", 12 | "ws": "^3.3.1" 13 | }, 14 | "devDependencies": {}, 15 | "author": "http://idlab.technology/#organization", 16 | "license": "MIT", 17 | "keywords": [] 18 | } 19 | --------------------------------------------------------------------------------