├── docs ├── _config.yml ├── google5f11c95fa18e972b.html └── assets │ ├── story1.png │ ├── story2.png │ ├── story3.png │ ├── story4.png │ ├── story5.png │ ├── story6.png │ ├── screen1.png │ ├── screen2.png │ ├── screen3.png │ ├── screen4.png │ └── reference.txt ├── webserver ├── public │ ├── js │ │ ├── .babelrc │ │ ├── reducers │ │ │ ├── index.js │ │ │ ├── config.js │ │ │ └── storyfinder.js │ │ ├── index.js │ │ ├── store │ │ │ └── configureStore.js │ │ ├── vis │ │ │ ├── helpers │ │ │ │ └── smoothLine.js │ │ │ └── transitions │ │ │ │ ├── removeDeleted.js │ │ │ │ ├── showNew.js │ │ │ │ └── moveExisting.js │ │ ├── package.json │ │ ├── templates │ │ │ ├── relations │ │ │ │ ├── relations.hbs │ │ │ │ └── relation.hbs │ │ │ ├── sites │ │ │ │ └── sites.hbs │ │ │ ├── graph │ │ │ │ └── title.hbs │ │ │ ├── search │ │ │ │ └── results.hbs │ │ │ └── nodes │ │ │ │ └── create.hbs │ │ ├── constants │ │ │ └── ActionTypes.js │ │ ├── libs │ │ │ ├── pagerank.js │ │ │ └── shortestpaths.js │ │ ├── search.js │ │ ├── register │ │ │ └── register.js │ │ └── actions │ │ │ └── StoryfinderActions.js │ ├── fonts │ │ ├── FontAwesome.otf │ │ ├── fontawesome-webfont.eot │ │ ├── fontawesome-webfont.ttf │ │ ├── fontawesome-webfont.woff │ │ └── fontawesome-webfont.woff2 │ ├── css │ │ └── svg.css │ ├── login.html │ ├── register.html │ └── _index.html ├── docker-deploy.sh ├── init_dev.sh ├── libs │ ├── evallog.js │ ├── tokenizer.js │ ├── corenlp.js │ ├── changelog.js │ └── globalgraph.js ├── views │ ├── layouts │ │ └── main.handlebars │ └── Articles │ │ └── get.handlebars ├── setenv.sh ├── docker │ ├── Dockerfile-dev │ ├── docker-compose-build.yml │ ├── docker-compose.yml │ └── docker-compose-dev.yml ├── Dockerfile ├── models │ ├── Relationtype.js │ ├── Collection.js │ ├── Ngram.js │ ├── User.js │ ├── EntitySentence.js │ ├── RelationSentence.js │ └── ArticleEntity.js ├── controllers │ ├── components │ │ ├── StopwordComponent.js │ │ ├── GermanerComponent.js │ │ ├── KeywordComponent.js │ │ ├── CorenlpComponent.js │ │ └── RandomforestComponent.js │ ├── UsersController.js │ ├── ArticlesController.js │ ├── GraphsController.js │ └── RelationsController.js ├── package.json ├── README.md ├── data │ └── stopwords │ │ └── german.txt └── server.js ├── .gitattributes ├── plugin ├── src │ ├── icon-48.png │ ├── icon-500.png │ ├── icon-red-48.png │ ├── icon-red-500.png │ ├── js-contentstyle │ │ ├── package.json │ │ ├── Gruntfile.js │ │ └── less │ │ │ └── storyfinder.less │ ├── js-backgroundscript │ │ └── package.json │ ├── js-contentscript │ │ └── package.json │ ├── popup.html │ ├── manifest.json │ ├── options.js │ ├── options.html │ ├── menu.js │ ├── popup.js │ ├── icon.html │ ├── menu.html │ └── contentstyle.css ├── notes.txt ├── package.json └── README.md ├── README.md └── .gitignore /docs/_config.yml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /webserver/public/js/.babelrc: -------------------------------------------------------------------------------- 1 | {"presets":["es2015"]} 2 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | *.jpg -text 3 | *.png -text 4 | -------------------------------------------------------------------------------- /docs/google5f11c95fa18e972b.html: -------------------------------------------------------------------------------- 1 | google-site-verification: google5f11c95fa18e972b.html -------------------------------------------------------------------------------- /docs/assets/story1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uhh-lt/storyfinder/HEAD/docs/assets/story1.png -------------------------------------------------------------------------------- /docs/assets/story2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uhh-lt/storyfinder/HEAD/docs/assets/story2.png -------------------------------------------------------------------------------- /docs/assets/story3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uhh-lt/storyfinder/HEAD/docs/assets/story3.png -------------------------------------------------------------------------------- /docs/assets/story4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uhh-lt/storyfinder/HEAD/docs/assets/story4.png -------------------------------------------------------------------------------- /docs/assets/story5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uhh-lt/storyfinder/HEAD/docs/assets/story5.png -------------------------------------------------------------------------------- /docs/assets/story6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uhh-lt/storyfinder/HEAD/docs/assets/story6.png -------------------------------------------------------------------------------- /plugin/src/icon-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uhh-lt/storyfinder/HEAD/plugin/src/icon-48.png -------------------------------------------------------------------------------- /docs/assets/screen1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uhh-lt/storyfinder/HEAD/docs/assets/screen1.png -------------------------------------------------------------------------------- /docs/assets/screen2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uhh-lt/storyfinder/HEAD/docs/assets/screen2.png -------------------------------------------------------------------------------- /docs/assets/screen3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uhh-lt/storyfinder/HEAD/docs/assets/screen3.png -------------------------------------------------------------------------------- /docs/assets/screen4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uhh-lt/storyfinder/HEAD/docs/assets/screen4.png -------------------------------------------------------------------------------- /plugin/src/icon-500.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uhh-lt/storyfinder/HEAD/plugin/src/icon-500.png -------------------------------------------------------------------------------- /plugin/src/icon-red-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uhh-lt/storyfinder/HEAD/plugin/src/icon-red-48.png -------------------------------------------------------------------------------- /plugin/src/icon-red-500.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uhh-lt/storyfinder/HEAD/plugin/src/icon-red-500.png -------------------------------------------------------------------------------- /webserver/public/fonts/FontAwesome.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uhh-lt/storyfinder/HEAD/webserver/public/fonts/FontAwesome.otf -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # storyfinder 2 | Storyfinder - A Browser Plugin and Server Backend for Personalized Knowledge- and Information Management 3 | -------------------------------------------------------------------------------- /webserver/public/js/reducers/index.js: -------------------------------------------------------------------------------- 1 | export { default as storyfinder } from './storyfinder'; 2 | export { default as config } from './config'; -------------------------------------------------------------------------------- /webserver/public/fonts/fontawesome-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uhh-lt/storyfinder/HEAD/webserver/public/fonts/fontawesome-webfont.eot -------------------------------------------------------------------------------- /webserver/public/fonts/fontawesome-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uhh-lt/storyfinder/HEAD/webserver/public/fonts/fontawesome-webfont.ttf -------------------------------------------------------------------------------- /webserver/public/fonts/fontawesome-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uhh-lt/storyfinder/HEAD/webserver/public/fonts/fontawesome-webfont.woff -------------------------------------------------------------------------------- /webserver/public/fonts/fontawesome-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uhh-lt/storyfinder/HEAD/webserver/public/fonts/fontawesome-webfont.woff2 -------------------------------------------------------------------------------- /webserver/public/js/index.js: -------------------------------------------------------------------------------- 1 | var App = require('./app.js') 2 | ; 3 | 4 | import configureStore from './store/configureStore'; 5 | 6 | const store = configureStore(); 7 | 8 | var app = new App(store); -------------------------------------------------------------------------------- /webserver/docker-deploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | v=0.0.10 4 | 5 | docker login --username=remstef 6 | 7 | docker build -t remstef/storyfinder:$v . 8 | 9 | docker push remstef/storyfinder:$v 10 | -------------------------------------------------------------------------------- /webserver/init_dev.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | apt-get update && apt-get install -y libcairo2-dev libjpeg62-turbo-dev libpango1.0-dev libgif-dev build-essential 4 | 5 | cd /usr/src/app && npm install 6 | 7 | cd /usr/src/app/public/js && npm install 8 | -------------------------------------------------------------------------------- /webserver/libs/evallog.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs') 2 | ; 3 | 4 | module.exports = function(){ 5 | function log(msg){ 6 | //Used for logging events during evaluation 7 | /*fs.appendFile('./data/logs/log.txt', "\n" + (new Date()) + ' ' + msg, function (err) { 8 | console.log(err); 9 | });*/ 10 | } 11 | 12 | this.log = log; 13 | } -------------------------------------------------------------------------------- /webserver/public/js/reducers/config.js: -------------------------------------------------------------------------------- 1 | import Immutable from 'immutable'; 2 | import * as types from '../constants/ActionTypes'; 3 | 4 | const initialState = Immutable.Map({ 5 | 'user-id': 1, 6 | 'server-url': 'http://127.0.0.1:3055' 7 | }); 8 | 9 | export default function config(state = initialState, action) { 10 | switch (action.type) { 11 | 12 | } 13 | return state; 14 | } -------------------------------------------------------------------------------- /webserver/views/layouts/main.handlebars: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Storyfinder 8 | 11 | 12 | 13 | 14 | {{{body}}} 15 | 16 | 17 | -------------------------------------------------------------------------------- /plugin/src/js-contentstyle/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "less2", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "Gruntfile.js", 6 | "dependencies": {}, 7 | "devDependencies": { 8 | "grunt": "^1.0.1", 9 | "grunt-contrib-less": "^1.4.0", 10 | "grunt-contrib-watch": "^1.0.0", 11 | "jit-grunt": "^0.10.0" 12 | }, 13 | "scripts": { 14 | "test": "echo \"Error: no test specified\" && exit 1" 15 | }, 16 | "author": "", 17 | "license": "ISC" 18 | } 19 | -------------------------------------------------------------------------------- /webserver/setenv.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | export COOKIE_SECRET="1337" 4 | 5 | export MYSQL_HOST="mysql" # "localhost" # "mysql" # 6 | export MYSQL_PORT="3306" 7 | 8 | export CORENLP_HOST="corenlp" # "localhost" # "corenlp" # 9 | export CORENLP_PORT="9000" 10 | 11 | export GERMANER_HOST="germaner" # "localhost" # "ltpc1" 12 | export GERMANER_PORT="8080" 13 | 14 | export NER="corenlp" # germaner 15 | export NO_KEYWORDS="" # true 16 | 17 | export CHROME_ID="hnndfanecdfnonofigcceaahflgfpgbd" # chrome -------------------------------------------------------------------------------- /plugin/notes.txt: -------------------------------------------------------------------------------- 1 | 2 | # run node in docker 3 | 4 | docker run --rm -ti --name sf-plugin-build -v $(pwd):/usr/local/app -w /usr/local/app node:6 bash 5 | 6 | $> npm install -g browserify grunt crx 7 | $> cd /usr/local/app/src/plugin 8 | 9 | # follow instructions in src/README.md 10 | 11 | # prepare release 12 | 13 | $> cd /usr/local/app/plugin 14 | $> npm install && npm run-script copy 15 | $> cd.. && crx pack plugin\dist -o release\storyfinder.xyz.crx -p storyfinder\release\storyfinder-plugin.pem 16 | -------------------------------------------------------------------------------- /plugin/src/js-backgroundscript/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "storyfinder_pageworker", 3 | "version": "0.0.1", 4 | "description": "Storyfinder Pageworker", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "dependencies": { 12 | "async": "^2.0.1" 13 | }, 14 | "devDependencies": { 15 | "babelify": "^7.3.0", 16 | "handlebars": "^4.0.6", 17 | "hbsfy": "^2.7.0", 18 | "readability-node": "^0.1.0" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /plugin/src/js-contentscript/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "storyfinder_content_script", 3 | "version": "0.0.1", 4 | "description": "Storyfinder content script", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "dependencies": { 12 | "async": "^2.0.1", 13 | "dom-delegate": "^2.0.3", 14 | "escape-string-regexp": "^1.0.5", 15 | "html5": "^1.0.5", 16 | "lodash": "^4.15.0", 17 | "readability": "^0.1.0" 18 | }, 19 | "devDependencies": { 20 | "babelify": "^7.3.0", 21 | "handlebars": "^4.0.6", 22 | "hbsfy": "^2.7.0", 23 | "readability-node": "^0.1.0" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /plugin/src/js-contentstyle/Gruntfile.js: -------------------------------------------------------------------------------- 1 | module.exports = function(grunt) { 2 | require('jit-grunt')(grunt); 3 | 4 | grunt.initConfig({ 5 | less: { 6 | development: { 7 | options: { 8 | compress: true, 9 | yuicompress: true, 10 | optimization: 2 11 | }, 12 | files: { 13 | "../contentstyle.css": "less/storyfinder.less" // destination file and source file 14 | } 15 | } 16 | }, 17 | watch: { 18 | styles: { 19 | files: ['less/**/*.less'], // which files to watch 20 | tasks: ['less'], 21 | options: { 22 | nospawn: true 23 | } 24 | } 25 | } 26 | }); 27 | 28 | grunt.registerTask('default', ['less', 'watch']); 29 | }; -------------------------------------------------------------------------------- /webserver/docker/Dockerfile-dev: -------------------------------------------------------------------------------- 1 | FROM node:6 2 | 3 | RUN set -ex \ 4 | && DEBIAN_FRONTEND=noninteractive \ 5 | && apt-get update \ 6 | && apt-get install -y --no-install-recommends apt-utils locales libcairo2-dev libjpeg62-turbo-dev libpango1.0-dev libgif-dev build-essential \ 7 | && sed -i -e 's/# en_US.UTF-8 UTF-8/en_US.UTF-8 UTF-8/' /etc/locale.gen \ 8 | && dpkg-reconfigure --frontend=noninteractive locales \ 9 | && update-locale LANG=en_US.UTF-8 10 | ENV LANG en_US.UTF-8 11 | 12 | VOLUME /usr/src/app 13 | 14 | WORKDIR /usr/src/app 15 | 16 | EXPOSE 3055 17 | 18 | # use docker attach and "Ctrl-p + Ctrl-q" in order to attach to, and gracefully detach from the container or use "Ctrl-c" in order to cancel the process (once attached) 19 | CMD [ "bash" ] 20 | -------------------------------------------------------------------------------- /plugin/src/popup.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Storyfinder 5 | 6 | 33 | 34 |

35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /docs/assets/reference.txt: -------------------------------------------------------------------------------- 1 | @inproceedings{remusetal:2017:cikm, 2 | author = {Remus, Steffen and Kaufmann, Manuel and Ballweg, Kathrin and von Landesberger, Tatiana and Biemann, Chris}, 3 | title = {Storyfinder: Personalized Knowledge Base Construction and Management by Browsing the Web}, 4 | booktitle = {CIKM '17: Proceedings of the 2017 ACM on Conference on Information and Knowledge Management}, 5 | year = {2017}, 6 | address = {Singapore, Singapore}, 7 | pages = {2519--2522}, 8 | series = {CIKM} 9 | } 10 | 11 | 12 | @proceedings{Lim:2017:3132847, 13 | title = {CIKM '17: Proceedings of the 2017 ACM on Conference on Information and Knowledge Management}, 14 | year = {2017}, 15 | isbn = {978-1-4503-4918-5}, 16 | location = {Singapore, Singapore}, 17 | publisher = {ACM}, 18 | address = {New York, NY, USA}, 19 | } 20 | -------------------------------------------------------------------------------- /webserver/public/js/store/configureStore.js: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware, compose } from 'redux'; 2 | import rootReducer from '../reducers'; 3 | import { combineReducers } from 'redux'; 4 | import * as reducers from '../reducers'; 5 | import thunkMiddleware from 'redux-thunk'; 6 | 7 | const reducer = combineReducers(reducers); 8 | 9 | const enhancer = compose( 10 | applyMiddleware(thunkMiddleware) 11 | // Middleware you want to use in development: 12 | // applyMiddleware(d1, d2, d3), 13 | // Required! Enable Redux DevTools with the monitors you chose 14 | ); 15 | 16 | export default function configureStore(initialState) { 17 | // Note: only Redux >= 3.1.0 supports passing enhancer as third argument. 18 | // See https://github.com/rackt/redux/releases/tag/v3.1.0 19 | const store = createStore(reducer, initialState, enhancer); 20 | return store; 21 | } -------------------------------------------------------------------------------- /webserver/public/js/vis/helpers/smoothLine.js: -------------------------------------------------------------------------------- 1 | var d3 = require('d3') 2 | , _ = require('lodash') 3 | ; 4 | 5 | var lineFunction = d3.svg.line() 6 | .x(function (d) { return _.isNull(d)?0:d.x; }) 7 | .y(function (d) { return _.isNull(d)?0:d.y; }) 8 | .interpolate("basis"); 9 | 10 | module.exports = function(l, r){ 11 | var left = {x: l.x, y: l.y} 12 | , right = {x: r.x, y: r.y} 13 | ; 14 | 15 | if(left.x < right.x){ 16 | var t = left; 17 | left = right; 18 | right = t; 19 | } 20 | 21 | var lineData = []; 22 | 23 | lineData.push({ 24 | x: left.x, 25 | y: left.y 26 | }); 27 | 28 | lineData.push({ 29 | x: left.x + (right.x - left.x) * 0.25, 30 | y: right.y + (left.y - right.y) * 0.25 31 | }); 32 | 33 | lineData.push({ 34 | x: right.x, 35 | y: right.y 36 | }); 37 | 38 | lineData = _.reverse(lineData); 39 | 40 | return lineFunction(lineData); 41 | }; -------------------------------------------------------------------------------- /plugin/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "storyfinder-prepare-dist", 3 | "devDependencies": { 4 | "copyfiles": "latest", 5 | "bestzip": "latest", 6 | "crx": "latest", 7 | "rename-cli": "latest", 8 | "rmdir-cli": "latest" 9 | }, 10 | "scripts": { 11 | "build": "npm install -g grunt && npm install -g browserify && npm install && cd src/js-contentscript && npm install && browserify index.js -t babelify -t [hbsfy -t] -o ../contentscript.js && cd ../js-backgroundscript && npm install && browserify index.js -t babelify -t [hbsfy -t] -o ../backgroundscript.js && cd ../js-contentstyle && npm install && grunt less", 12 | "publish": "npm install && rmdir-cli dist && copyfiles -f src/* dist && copyfiles -f ../release/storyfinder-plugin.pem dist && rname dist/storyfinder-plugin.pem dist/key.pem && bestzip ../release/storyfinder.zip dist/* && crx pack dist -o ../release/storyfinder.crx -p ../release/storyfinder-plugin.pem" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /webserver/public/js/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "storyfinder", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "dependencies": { 12 | "async": "1.5.2", 13 | "babel-preset-es2015": "6.9.0", 14 | "base-64": "0.1.0", 15 | "color-thief": "2.2.2", 16 | "d3": "3.5.17", 17 | "dom-delegate": "2.0.3", 18 | "es6-promise": "3.2.1", 19 | "form-serialize": "0.7.1", 20 | "handlebars": "4.0.5", 21 | "hbsfy": "2.7.0", 22 | "hyphenation.de": "0.2.1", 23 | "hypher": "0.2.4", 24 | "immutable": "3.8.1", 25 | "isomorphic-fetch": "2.2.1", 26 | "lodash": "4.12.0", 27 | "pagerank": "2.0.0", 28 | "pagerank-js": "0.3.0", 29 | "redux": "3.5.2", 30 | "redux-devtools": "3.3.1", 31 | "redux-thunk": "2.1.0", 32 | "socket.io-client": "1.4.6", 33 | "webcola": "3.1.3" 34 | }, 35 | "devDependencies": { 36 | "babelify": "7.3.0" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /webserver/public/js/templates/relations/relations.hbs: -------------------------------------------------------------------------------- 1 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | {{#each Relations}} 22 | 23 | 24 | 31 | 32 | 33 | {{/each}} 34 | 35 |
LabelNeighbor
25 | {{#if Relationtype.label}} 26 | {{Relationtype.label}} 27 | {{else}} 28 | — 29 | {{/if}} 30 | {{neighbour.caption}}
36 | 37 | -------------------------------------------------------------------------------- /plugin/src/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 2, 3 | "name": "Storyfinder", 4 | "short_name": "Storyfinder", 5 | "version": "0.0.13", 6 | 7 | "description": "Graph based information and knowledge management of websites and entities.", 8 | "icons": { 9 | "48": "icon-48.png", 10 | "500": "icon-500.png" 11 | }, 12 | 13 | "browser_action": { 14 | "default_popup": "menu.html" 15 | }, 16 | 17 | "author": "Language Technology Group - University of Hamburg", 18 | "background": { 19 | "scripts": [ 20 | "backgroundscript.js" 21 | ] 22 | }, 23 | "content_scripts": [ 24 | { 25 | "matches": [""], 26 | "css": [ 27 | "contentstyle.css" 28 | ], 29 | "js": [ 30 | "contentscript.js" 31 | ] 32 | } 33 | ], 34 | "homepage_url": "https://uhh-lt.github.io/storyfinder/", 35 | "options_ui": { 36 | "page": "options.html" 37 | }, 38 | "permissions": [ 39 | "tabs", 40 | "storage", 41 | "webRequest", 42 | "", 43 | "contextMenus", 44 | "webNavigation" 45 | ], 46 | "web_accessible_resources": 47 | [ 48 | "popup.html" 49 | ] 50 | } 51 | -------------------------------------------------------------------------------- /plugin/src/options.js: -------------------------------------------------------------------------------- 1 | function saveChanges() { 2 | // Get a value saved in a form. 3 | var s = document.getElementById('server').value; 4 | 5 | chrome.storage.sync.get({ 6 | server: "" 7 | }, function(items) { 8 | if(items.server !== s) { 9 | // Save it using the Chrome extension storage API. 10 | chrome.storage.sync.set({ 11 | server: s, 12 | serverInitialized: true 13 | }, function() { 14 | chrome.runtime.sendMessage({type:"settings-changed"}); 15 | window.close(); 16 | }); 17 | } else { 18 | window.close(); 19 | } 20 | }); 21 | } 22 | 23 | function restoreOptions() { 24 | // Get a value from the Chrome extension storage API. 25 | chrome.storage.sync.get({ 26 | server: "http://example.org" 27 | }, function(items) { 28 | // Set a value in a form. 29 | document.getElementById('server').value = items.server; 30 | }); 31 | } 32 | 33 | document.addEventListener("DOMContentLoaded", restoreOptions); 34 | document.querySelector("form").addEventListener("submit", saveChanges); 35 | -------------------------------------------------------------------------------- /webserver/Dockerfile: -------------------------------------------------------------------------------- 1 | # 2 | # StoryFinder build 3 | # 4 | FROM node:6 5 | 6 | RUN set -ex \ 7 | && DEBIAN_FRONTEND=noninteractive \ 8 | && apt-get update \ 9 | && apt-get install -y --no-install-recommends apt-utils locales libcairo2-dev libjpeg62-turbo-dev libpango1.0-dev libgif-dev build-essential \ 10 | && sed -i -e 's/# en_US.UTF-8 UTF-8/en_US.UTF-8 UTF-8/' /etc/locale.gen \ 11 | && dpkg-reconfigure --frontend=noninteractive locales \ 12 | && update-locale LANG=en_US.UTF-8 \ 13 | && apt-get clean 14 | ENV LANG en_US.UTF-8 15 | 16 | RUN mkdir -p /usr/src/app 17 | 18 | WORKDIR /usr/src/app 19 | 20 | ENV NODE_ENV "development" 21 | 22 | ENV COOKIE_SECRET "s3cr31" 23 | 24 | COPY package.json /usr/src/app/ 25 | 26 | COPY . /usr/src/app 27 | 28 | RUN mkdir -p /usr/src/app/public/images 29 | 30 | RUN [ -e /usr/src/app/node_modules ] && rm -r -f /usr/src/app/node_modules || echo "nothing to do" 31 | 32 | RUN [ -e /usr/src/app/public/js/node_modules ] && rm -r -f /usr/src/app/public/js/node_modules || echo "nothing to do" 33 | 34 | RUN npm install 35 | 36 | RUN cd /usr/src/app/public/js && npm install 37 | 38 | EXPOSE 3055 39 | 40 | CMD [ "npm", "start" ] 41 | -------------------------------------------------------------------------------- /webserver/models/Relationtype.js: -------------------------------------------------------------------------------- 1 | var DatasourceMysql = require('../datasources/mysql.js') 2 | , async = require('async') 3 | , _ = require('lodash') 4 | ; 5 | 6 | module.exports = function(db){ 7 | //console.log('Relationtype'); 8 | var name = 'Relationtype' 9 | , table = 'relationtypes' 10 | , datasource = new DatasourceMysql(db, name, table) 11 | ; 12 | 13 | function find(relationtypes, callback){ 14 | datasource.find('list', { 15 | fields: ['id', 'label'], 16 | conditions: { 17 | id: relationtypes, 18 | is_deleted: 0 19 | } 20 | }, callback); 21 | } 22 | 23 | this.find = find; 24 | 25 | function findByLabel(label, collectionId, callback){ 26 | datasource.find('first', { 27 | fields: ['id', 'label', 'collection_id'], 28 | conditions: { 29 | label: label, 30 | collection_id: collectionId 31 | } 32 | }, callback); 33 | } 34 | 35 | this.findByLabel = findByLabel; 36 | 37 | function create(changelogId, relationtype, callback){ 38 | datasource.insert(changelogId, { 39 | values: { 40 | label: relationtype.label, 41 | collection_id: relationtype.collection_id, 42 | is_deleted: 0 43 | } 44 | }, callback); 45 | } 46 | 47 | this.create = create; 48 | } -------------------------------------------------------------------------------- /webserver/public/css/svg.css: -------------------------------------------------------------------------------- 1 | .background rect { 2 | transition: fill 1s ease; 3 | fill: #FFFFFF; 4 | } 5 | 6 | .link { 7 | stroke: #999; 8 | stroke-width: 1px; 9 | stroke-opacity: 1; 10 | fill: none; 11 | } 12 | 13 | .node { 14 | /*fill: #03A9FA;*/ 15 | fill: none; 16 | } 17 | 18 | .link.is-hidden { 19 | stroke-width: 0; 20 | } 21 | 22 | .label text { 23 | transition: fill 1s ease; 24 | fill: #FFFFFF; 25 | text-anchor: middle; 26 | font-size: 10px; 27 | font-family: Verdana; 28 | } 29 | 30 | .label rect { 31 | transition: fill 1s ease; 32 | stroke: none; 33 | fill: #8BC34A; 34 | } 35 | 36 | .label.topNode rect { 37 | transition: fill 1s ease; 38 | fill: #0288D1; 39 | } 40 | 41 | .label.focused rect { 42 | fill: #8BC34A !important; 43 | filter:url(#dropshadow); 44 | } 45 | 46 | .label.focused text { 47 | fill: #FFFFFF !important; 48 | } 49 | 50 | svg.grayscale .background rect { 51 | /*fill: #d7d7d7;*/ 52 | } 53 | 54 | svg.grayscale .label text { 55 | fill: #d7d7d7; 56 | } 57 | 58 | svg.grayscale .label rect { 59 | fill: #999999; 60 | } 61 | 62 | svg.grayscale .label.topNode rect { 63 | fill: #999999; 64 | } -------------------------------------------------------------------------------- /plugin/src/options.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 31 | 32 | 33 |
34 | 35 | 36 | 37 | 38 |
39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /webserver/public/js/templates/sites/sites.hbs: -------------------------------------------------------------------------------- 1 |
2 | {{#each Sites}} 3 |
4 |
5 |
6 |
{{last_visited}}
7 |

{{#if title}} 8 | {{title}} 9 | {{url}} 10 | {{else}} 11 | {{shortHost}} 12 | {{shortUrl}} 13 | {{/if}} 14 |

15 |
16 | {{#each Article.sentences}} 17 |
»{{{html}}}
18 | {{/each}} 19 | {{#if sentencesMore}} 20 |
21 |
View {{sentencesMore}} more sentences
22 |
Hide {{sentencesMore}} sentences
23 |
24 | {{/if}} 25 |
26 | 30 |
31 |
32 | {{/each}} 33 |
-------------------------------------------------------------------------------- /webserver/controllers/components/StopwordComponent.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash') 2 | , async = require('async') 3 | , fs = require('fs') 4 | , path = require('path') 5 | ; 6 | 7 | module.exports = function(){ 8 | var options = { 9 | path: path.join(__dirname,'/../..', '/data/stopwords/german.txt') 10 | } 11 | , stopwords = {} 12 | ; 13 | 14 | if(arguments[0]) 15 | options = _.defaults(arguments[0], options); 16 | 17 | /* 18 | Read stopword file 19 | 20 | Comments begin with vertical bar. Each stop | word is at the start of a line. 21 | Source: Snowball (http://snowball.tartarus.org/algorithms/german/stop.txt) 22 | 23 | stopword | comment 24 | stopword2 25 | 26 | |comment 27 | stopword3 | comment3 28 | */ 29 | function readStopwords(){ 30 | fs.readFileSync(options.path).toString().split("\n").forEach((row) => { 31 | row = row.split("|"); //Remove comments 32 | stopword = row[0].replace(/^\s+/g,'').replace(/\s+$/g,''); 33 | if(stopword.length > 0) 34 | stopwords[stopword] = true; 35 | }); 36 | 37 | console.log('Read ' + _.keys(stopwords).length + ' stopwords from ' + options.path); 38 | } 39 | 40 | if(!_.isEmpty(options.stopwords)) 41 | stopwords = options.stopwords; 42 | else 43 | readStopwords(); 44 | 45 | 46 | function is(token){ 47 | return !_.isUndefined(stopwords[token.toLowerCase()]); 48 | } 49 | 50 | this.is = is; 51 | } -------------------------------------------------------------------------------- /webserver/libs/tokenizer.js: -------------------------------------------------------------------------------- 1 | var async = require('async') 2 | , _ = require('lodash') 3 | ; 4 | 5 | //Treebank word tokenizer 6 | module.exports = new (function(){ 7 | var contractions2 = [ 8 | /(.)('ll|'re|'ve|n't|'s|'m|'d)\b/ig, 9 | /\b(can)(not)\b/ig, 10 | /\b(D)('ye)\b/ig, 11 | /\b(Gim)(me)\b/ig, 12 | /\b(Gon)(na)\b/ig, 13 | /\b(Got)(ta)\b/ig, 14 | /\b(Lem)(me)\b/ig, 15 | /\b(Mor)('n)\b/ig, 16 | /\b(T)(is)\b/ig, 17 | /\b(T)(was)\b/ig, 18 | /\b(Wan)(na)\b/ig]; 19 | 20 | var contractions3 = [ 21 | /\b(Whad)(dd)(ya)\b/ig, 22 | /\b(Wha)(t)(cha)\b/ig 23 | ]; 24 | 25 | this.tokenize = function(text) { 26 | contractions2.forEach(function(regexp) { 27 | text = text.replace(regexp,"$1 $2"); 28 | }); 29 | 30 | contractions3.forEach(function(regexp) { 31 | text = text.replace(regexp,"$1 $2 $3"); 32 | }); 33 | 34 | // most punctuation 35 | //@mod Umlaute hinzugefügt 36 | text = text.replace(/([^\w\.\'\-\/\+\<\>,&äöüßÄÖÜ])/g, " $1 "); 37 | 38 | // commas if followed by space 39 | text = text.replace(/(,)/g, " $1"); 40 | 41 | // single quotes if followed by a space 42 | text = text.replace(/('\s)/g, " $1"); 43 | 44 | // periods before newline or end of string 45 | text = text.replace(/\. *(\n|$)/g, " . "); 46 | 47 | return _.without(text.split(/\s+/), ''); 48 | }; 49 | })(); -------------------------------------------------------------------------------- /webserver/views/Articles/get.handlebars: -------------------------------------------------------------------------------- 1 |
2 |
3 | This page contains the cached version of the article »{{Site.title}}« on »{{Site.host}}«, created on {{Article.created}}. 4 |

5 |

6 |
Host
7 |
{{#if Site.favicon}} {{/if}}{{Site.host}}
8 | 9 |
Source
10 |
{{Site.url}}
11 | 12 |
First visit
13 |
{{Site.created}}
14 | 15 |
Last visit
16 |
{{Site.last_visited}}
17 | 18 |
Abstract
19 |
{{Article.excerpt}}
20 | 21 |
Screenshot
22 |
23 | 24 |
Article screenshot
25 |
26 |
27 |
28 | 29 |

{{#if Article.title}}{{Article.title}}{{else}}{{Site.title}}{{/if}}

30 | {{{Article.raw}}} 31 |
-------------------------------------------------------------------------------- /webserver/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "storyfinder-server", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "build": "npm install -g browserify && npm install && cd public/js && npm install && browserify index.js -t babelify -t [hbsfy -t] -o storyfinder.js && browserify register/register.js -t babelify -t [hbsfy -t] -o register.js", 9 | "publish": "docker login && docker build -t uhhlt/storyfinder:latest . && docker push uhhlt/storyfinder:latest" 10 | }, 11 | "author": "", 12 | "license": "ISC", 13 | "dependencies": { 14 | "async": "^1.5.2", 15 | "bcrypt": "^0.8.7", 16 | "body-parser": "^1.15.1", 17 | "color-thief": "^2.2.2", 18 | "compute-cosine-distance": "^1.0.0", 19 | "connect-ensure-login": "^0.1.1", 20 | "cookie-session": "^2.0.0-alpha.1", 21 | "cookies": "^0.6.1", 22 | "couchbase": "^2.1.6", 23 | "escape-string-regexp": "^1.0.5", 24 | "express": "^4.13.4", 25 | "express-handlebars": "^3.0.0", 26 | "hiredis": "^0.4.1", 27 | "lodash": "^4.12.0", 28 | "method-override": "^2.3.5", 29 | "mysql": "^2.10.2", 30 | "node-kmeans": "^1.1.0", 31 | "node-stanford-postagger": "0.0.1", 32 | "pagerank-js": "^0.3.0", 33 | "passport": "^0.3.2", 34 | "passport-http": "^0.3.0", 35 | "passport-local": "^1.0.0", 36 | "redis": "^2.5.3", 37 | "request": "^2.72.0", 38 | "serve-static": "^1.10.2", 39 | "socket.io": "^1.4.6" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /webserver/public/js/constants/ActionTypes.js: -------------------------------------------------------------------------------- 1 | /*export const SHOW_SITE = 'SHOW_SITE'; 2 | export const SHOW_GLOBAL = 'SHOW_GLOBAL'; 3 | export const SELECT_NODE = 'SELECT_NODE'; 4 | export const OPEN_NODE = 'OPEN_NODE'; 5 | export const EXPAND_NODE = 'EXPAND_NODE'; 6 | export const DELETE_NODE = 'DELETE_NODE'; 7 | export const ADD_NODE = 'ADD_NODE';*/ 8 | export const SHOW_RELATION = 'SHOW_RELATION'; 9 | export const TO_RELATION = 'TO_RELATION'; 10 | export const REQUEST_RELATION = 'REQUEST_RELATION'; 11 | export const RECEIVE_RELATION = 'RECEIVE_RELATION'; 12 | export const TO_GRAPH = 'TO_GRAPH'; 13 | export const SHOW_GRAPH = 'SHOW_GRAPH'; 14 | export const TO_LOCALGRAPH = 'TO_LOCALGRAPH'; 15 | export const TO_GROUPGRAPH = 'TO_GROUPGRAPH'; 16 | export const SHOW_LOCALGRAPH = 'SHOW_LOCALGRAPH'; 17 | export const SHOW_GROUPGRAPH = 'SHOW_GROUPGRAPH'; 18 | export const TO_GLOBAL = 'TO_GLOBAL'; 19 | export const SHOW_GLOBAL = 'SHOW_GLOBAL'; 20 | export const REQUEST_GLOBAL = 'REQUEST_GLOBAL'; 21 | export const RECEIVE_GLOBAL = 'RECEIVE_GLOBAL'; 22 | export const REQUEST_DELETE_NODE = 'REQUEST_DELETE_NODE'; 23 | export const RECEIVE_DELETE_NODE = 'RECEIVE_DELETE_NODE'; 24 | export const REQUEST_NEIGHBOURS = 'REQUEST_NEIGHBOURS'; 25 | export const RECEIVE_NEIGHBOURS = 'RECEIVE_NEIGHBOURS'; 26 | export const CREATE_NODE = 'CREATE_NODE'; 27 | export const REQUEST_SAVE_NODE = 'REQUEST_SAVE_NODE'; 28 | export const RECEIVE_SAVE_NODE = 'RECEIVE_SAVE_NODE'; 29 | export const REQUEST_CREATE_RELATION = 'REQUEST_CREATE_RELATION'; 30 | export const RECEIVE_CREATE_RELATION = 'RECEIVE_CREATE_RELATION'; -------------------------------------------------------------------------------- /plugin/src/menu.js: -------------------------------------------------------------------------------- 1 | var buttonParse = document.getElementById("button-parse"); 2 | var buttonSidebar = document.getElementById("button-sidebar"); 3 | var buttonReadability = document.getElementById("button-readability"); 4 | var checkboxHighlight = document.getElementById("checkbox-highlight"); 5 | 6 | // display the correct status 7 | chrome.storage.sync.get({ 8 | highlightEntities: false, 9 | }, function (items) { 10 | checkboxHighlight.checked = items.highlightEntities; 11 | }); 12 | 13 | buttonParse.addEventListener("click", function() { 14 | chrome.storage.sync.get({ 15 | userInitialized: false, 16 | serverInitialized: false 17 | }, function (items) { 18 | if (!items.serverInitialized && !items.userInitialized) { 19 | alert("To use this function, please provide a Server URL in Extensions > Storyfinder > Options and log into your account."); 20 | } else { 21 | chrome.runtime.sendMessage({type:"force-parse-site"}); 22 | window.close(); 23 | } 24 | }); 25 | }); 26 | 27 | buttonSidebar.addEventListener("click", function() { 28 | chrome.runtime.sendMessage({type:"toggle-sidebar"}); 29 | window.close(); 30 | }); 31 | 32 | buttonReadability.addEventListener("click", function() { 33 | chrome.runtime.sendMessage({type:"do-readability"}); 34 | window.close(); 35 | }); 36 | 37 | checkboxHighlight.addEventListener("change", function() { 38 | chrome.storage.sync.set({ 39 | highlightEntities: checkboxHighlight.checked 40 | }, function() { 41 | chrome.runtime.sendMessage({type:"highlight-changed", checked: checkboxHighlight.checked}); 42 | }); 43 | }); -------------------------------------------------------------------------------- /webserver/public/js/templates/graph/title.hbs: -------------------------------------------------------------------------------- 1 | {{#if loading}} 2 |
Parsing...
3 | {{else}} 4 | {{#if site_ids}} 5 | {{#if groupsize}} 6 | arrow_back 7 |
Group of {{groupsize}} sites 8 | {{log sites}} 9 | {{#each sites}} 10 | {{#if @index}} 11 | , 12 | {{/if}} 13 | 14 | {{#if title}} 15 | {{title}} 16 | {{else}} 17 | {{url}} 18 | {{/if}} 19 | 20 | {{/each}} 21 | 22 |
23 | {{else}} 24 |
25 | {{/if}} 26 | {{else}} 27 | {{#if site_id}} 28 | {{#if site}} 29 | arrow_back 30 | {{#if site.title}} 31 |
{{site.title}}{{site.url}}
32 | {{else}} 33 |
{{site.host}}{{site.url}}
34 | {{/if}} 35 | {{else}} 36 |
37 | {{/if}} 38 | {{else}} 39 | menu 40 |
Global network
41 | {{/if}} 42 | {{/if}} 43 | 44 | searchclose 45 | 46 | {{/if}} -------------------------------------------------------------------------------- /plugin/src/popup.js: -------------------------------------------------------------------------------- 1 | var baseUrl = ""; 2 | 3 | chrome.runtime.onMessage.addListener(function(msg, sender){ 4 | switch (msg.type) { 5 | case "msg": 6 | document.querySelector('iframe.active').contentWindow.postMessage(msg.data, '*'); 7 | break; 8 | case "settings-changed": 9 | window.location.reload(); 10 | break; 11 | } 12 | }); 13 | 14 | chrome.storage.sync.get({ 15 | server: "", 16 | serverInitialized: false 17 | }, function(items) { 18 | if(!items.serverInitialized || (items.serverInitialized && items.server === "")) { 19 | var message = document.getElementById("message"); 20 | message.innerHTML = "The Storyfinder Server has not been set! Please go to Extensions > Storyfinder > Options and set the Server URL."; 21 | return; 22 | } 23 | 24 | baseUrl = items.server; 25 | var iframe = document.createElement('iframe'); 26 | iframe.setAttribute('src', baseUrl); 27 | iframe.classList.add("active"); 28 | iframe.frameBorder = "none"; 29 | 30 | 31 | document.body.appendChild(iframe); 32 | 33 | iframe.onload = function() { 34 | chrome.runtime.sendMessage({type: 'onAttach'}); 35 | } 36 | 37 | window.addEventListener("message", function(event){ 38 | var origin = event.origin || event.originalEvent.origin; 39 | if(baseUrl !== '' && baseUrl.substr(0, origin.length) !== origin){ 40 | // alert('Wrong origin: ' + baseUrl.substr(0, origin.length) + ' !== ' + origin); 41 | return; 42 | } 43 | 44 | if(event.data[0] === 'msg'){ 45 | chrome.runtime.sendMessage({type: 'msg', data: event.data[1]}); 46 | } 47 | }, false); 48 | }); 49 | 50 | 51 | -------------------------------------------------------------------------------- /webserver/public/js/templates/search/results.hbs: -------------------------------------------------------------------------------- 1 |
2 | {{#if Entities}} 3 |

Entities

4 |
5 | {{#each Entities}} 6 |
{{caption}}
7 | {{/each}} 8 |
9 |
10 | {{/if}} 11 | {{#if Sites}} 12 |

Sites

13 |
14 | {{#each Sites}} 15 |
16 |
17 |
18 |
{{last_visited}}
19 |

{{#if title}} 20 | {{title}} 21 | {{url}} 22 | {{else}} 23 | {{shortHost}} 24 | {{shortUrl}} 25 | {{/if}} 26 |

27 |
28 | {{#each Article.Sentences}} 29 |
»{{{this}}}
30 | {{/each}} 31 | {{#if sentencesMore}} 32 |
33 |
+ {{sentencesMore}} more sentences
34 |
35 | {{/if}} 36 |
37 | 42 |
43 |
44 | {{/each}} 45 |
46 |
47 | {{/if}} 48 | {{#unless Entities}} 49 | {{#unless Sites}} 50 | No results found! 51 | {{/unless}} 52 | {{/unless}} -------------------------------------------------------------------------------- /webserver/docker/docker-compose-build.yml: -------------------------------------------------------------------------------- 1 | ## 2 | # 3 | ## 4 | version: '2.1' 5 | 6 | networks: 7 | sf-net: 8 | 9 | services: 10 | mysqlserver: 11 | image: "mysql:5.5" 12 | environment: 13 | - MYSQL_RANDOM_ROOT_PASSWORD=yes 14 | - MYSQL_DATABASE=storyfinder 15 | - MYSQL_USER=storyfinder 16 | - MYSQL_PORT=3306 17 | - MYSQL_PASSWORD=storyfinder 18 | volumes: 19 | - ${PWD}/mysql-data:/var/lib/mysql 20 | command: ["--character-set-server=utf8", "--collation-server=utf8_bin"] 21 | healthcheck: 22 | test: ["CMD", "mysqladmin" ,"ping", "-h", "localhost", "-pstoryfinder", "-ustoryfinder"] 23 | interval: 20s 24 | timeout: 10s 25 | retries: 3 26 | networks: 27 | sf-net: 28 | aliases: 29 | - mysql 30 | 31 | corenlpserver: 32 | image: "kisad/corenlp-german:latest" 33 | healthcheck: 34 | test: wget --post-data 'The quick brown fox jumped over the lazy dog.' 'localhost:9000/?properties={"annotators":"tokenize,ssplit,pos,ner","outputFormat":"json"}' -O - 35 | interval: 90s 36 | timeout: 10s 37 | retries: 3 38 | mem_limit: 4g 39 | memswap_limit: 5g 40 | networks: 41 | sf-net: 42 | aliases: 43 | - corenlp 44 | 45 | webserver: 46 | depends_on: 47 | mysqlserver: 48 | condition: service_healthy 49 | corenlpserver: 50 | condition: service_healthy 51 | build: 52 | context: ../ 53 | dockerfile: Dockerfile 54 | ports: 55 | - "3055:3055" 56 | environment: 57 | - NODE_ENV=production 58 | - COOKIE_SECRET=92bls37gfc 59 | - NER=corenlp 60 | - NO_KEYWORDS=T 61 | restart: unless-stopped 62 | mem_limit: 4g 63 | memswap_limit: 5g 64 | networks: 65 | sf-net: 66 | -------------------------------------------------------------------------------- /webserver/public/js/templates/nodes/create.hbs: -------------------------------------------------------------------------------- 1 |
2 | close 3 | save 4 |
New element
5 |
6 |
7 |
8 |
9 | 10 | 11 |
12 | Please select a type: 13 |
14 | 18 |
19 |
20 | 24 |
25 |
26 | 30 |
31 |
32 | 36 |
37 | 38 | 42 |
43 | 47 |
48 |
49 | 53 |
54 |
55 |
-------------------------------------------------------------------------------- /plugin/README.md: -------------------------------------------------------------------------------- 1 | # Recommended workflow 2 | 3 | 0. provide the private key under /storyfinder/release/storyfinder-plugin.pem 4 | 1. make changes to the code 5 | 2. increase version number in /storyfinder/src/manifest.json 6 | 3. automatically build the plugin with npm run build 7 | 4. automatically publish the plugin with npm run publish 8 | 5. upload the generated zip file /storyfinder/release/storyfinder.zip to the google developer console 9 | 10 | # Build the plugin 11 | 12 | ## Automatic build 13 | this will automatically build the files: backgroundscript.js, contentscript.js, contentstyle.css 14 | 15 | ``` 16 | $ cd /storyfinder/plugin 17 | $ npm run build 18 | ``` 19 | 20 | ## Manual build 21 | 22 | ### Build the contentscript 23 | 24 | ``` 25 | $ cd /storyfinder/plugin/src/js-contentscript 26 | $ npm install 27 | $ browserify index.js -t babelify -t [hbsfy -t] -o ../contentscript.js 28 | ``` 29 | 30 | ### Build the backgroundscript 31 | 32 | ``` 33 | $ cd /storyfinder/plugin/src/js-backgroundscript 34 | $ npm install 35 | $ browserify index.js -t babelify -t [hbsfy -t] -o ../backgroundscript.js 36 | ``` 37 | 38 | ### Build the contentstyle 39 | 40 | ``` 41 | $ cd /storyfinder/plugin/src/js-contentstyle 42 | $ npm install 43 | $ grunt less 44 | ``` 45 | 46 | # Publish the plugin 47 | 48 | ## Automatic publish 49 | this will automatically create storyfinder.crx and storyfinder.zip in /storyfinder/release 50 | 51 | ``` 52 | $ cd /storyfinder/plugin 53 | $ npm run publish 54 | ``` 55 | 56 | ## Manual publish 57 | 58 | ### via command line with chrome 59 | 60 | ``` 61 | $ chrome.exe --pack-extension=/storyfinder/plugin/src --pack-extension-key=path/to/storyfinder-plugin.pem 62 | ``` 63 | 64 | ### via chrome 65 | 1. navigate to chrome://extensions/ 66 | 2. click pack extension 67 | 3. provide /storyfinder/plugin/src as the path to the extension 68 | provide /path/to/storyfinder-plugin.pem as the path to the private key -------------------------------------------------------------------------------- /webserver/models/Collection.js: -------------------------------------------------------------------------------- 1 | var DatasourceMysql = require('../datasources/mysql.js') 2 | , async = require('async') 3 | , _ = require('lodash') 4 | ; 5 | 6 | module.exports = function(db){ 7 | //console.log('Collection'); 8 | var name = 'Collection' 9 | , table = 'collections' 10 | , datasource = new DatasourceMysql(db, name, table) 11 | ; 12 | 13 | function findById(id, callback){ 14 | datasource.find(_.isArray(id)?'all':'first', { 15 | fields: ['id', 'user_id', 'is_default', 'name', 'created', 'modified'], 16 | conditions: { 17 | id: id, 18 | is_deleted: 0 19 | }, 20 | order: 'name ASC' 21 | }, (err, result) => { 22 | if(err) 23 | return setImmediate(() => callback(err)); 24 | 25 | setImmediate(() => callback(null, result)); 26 | }); 27 | } 28 | 29 | this.findById = findById; 30 | 31 | function getDefault(userId, callback){ 32 | datasource.find('first', { 33 | fields: ['id'], 34 | conditions: { 35 | user_id: userId, 36 | is_default: 1, 37 | is_deleted: 0 38 | } 39 | }, (err, result) => { 40 | if(err) 41 | return setImmediate(() => callback(err)); 42 | 43 | if(result != null) 44 | return setImmediate(() => callback(null, result)); 45 | 46 | createDefault(userId, callback); 47 | }); 48 | } 49 | 50 | this.getDefault = getDefault; 51 | 52 | function createDefault(userId, callback){ 53 | var date = (new Date()).toISOString().slice(0,19).replace('T',' '); 54 | 55 | db.query('INSERT INTO `collections` (user_id, is_default, name, created, modified, is_deleted) VALUES (?, 1, ?, ?, ?, 0)', [ 56 | userId, 57 | 'default', 58 | date, 59 | date 60 | ], function(err, res){ 61 | if(err) 62 | return setImmediate(() => { 63 | callback(err); 64 | }); 65 | 66 | setImmediate(() => { 67 | callback(null, { 68 | id: res.insertId 69 | }); 70 | }); 71 | }); 72 | } 73 | } 74 | 75 | -------------------------------------------------------------------------------- /webserver/public/login.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Storyfinder 8 | 11 | 12 | 13 |
14 |
15 |
16 |
17 | 23 |
24 | 25 | 26 | 27 |
28 |

StoryFinder

29 |
30 |
31 |
32 | 33 | 34 | 35 |
36 |
37 | 38 | 39 | 40 |
41 | 42 | 43 |
44 |

45 | New to StoryFinder?
Click here to create a new account. 46 |

47 |
48 |
49 |
50 |
51 | 52 | 53 | -------------------------------------------------------------------------------- /webserver/libs/corenlp.js: -------------------------------------------------------------------------------- 1 | var request = require('request') 2 | , _ = require('lodash') 3 | , async = require('async') 4 | ; 5 | 6 | module.exports = function(){ 7 | var options = _.defaults(arguments[0], { 8 | host: 'localhost', 9 | port: '9000' 10 | }); 11 | 12 | var defaultProperties = {}; 13 | if(typeof arguments[1] != 'undefined' && arguments[1] != null) 14 | defaultProperties = _.defaults(arguments[1], defaultProperties); 15 | 16 | function tag(data){ 17 | var properties = {}; 18 | 19 | if(arguments.length == 3) 20 | properties = arguments[1]; 21 | 22 | properties = _.defaults(properties, defaultProperties); 23 | 24 | var callback = arguments[arguments.length - 1]; 25 | 26 | request({ 27 | uri: 'http://' + options.host + ':' + options.port + '/?properties=' + encodeURIComponent(JSON.stringify(properties)), 28 | method: 'POST', 29 | body: data, 30 | contentType: 'text/plain' 31 | }, function (error, response, body) { 32 | 33 | body = body.replace(/\\n/g, "\\n") 34 | .replace(/\\'/g, "\\'") 35 | .replace(/\\"/g, '\\"') 36 | .replace(/\\&/g, "\\&") 37 | .replace(/\\r/g, "\\r") 38 | .replace(/\\t/g, "\\t") 39 | .replace(/\\b/g, "\\b") 40 | .replace(/\\f/g, "\\f") 41 | .replace(/[\u0000-\u0019]+/g,"") 42 | ; 43 | 44 | if (!error && response.statusCode == 200) { 45 | setImmediate(function(){ 46 | callback(null, JSON.parse(body)); 47 | }); 48 | }else{ 49 | callback(error); 50 | } 51 | }); 52 | } 53 | 54 | this.tag = tag; 55 | 56 | function pos(data){ 57 | var properties = { 58 | "annotators": "pos" 59 | }; 60 | 61 | if(arguments.length == 3) 62 | properties = arguments[1]; 63 | 64 | properties = _.defaults(properties, defaultProperties); 65 | 66 | var callback = arguments[arguments.length - 1]; 67 | 68 | tag(data, properties, callback); 69 | } 70 | 71 | this.pos = pos; 72 | }; -------------------------------------------------------------------------------- /webserver/models/Ngram.js: -------------------------------------------------------------------------------- 1 | var DatasourceMysql = require('../datasources/mysql.js') 2 | , async = require('async') 3 | , _ = require('lodash') 4 | ; 5 | 6 | module.exports = function(db){ 7 | //console.log('NGram'); 8 | var name = 'Ngram' 9 | , models = _.defaults({Ngram: this}, arguments[1] || {}) 10 | , table = 'ngrams' 11 | , datasource = new DatasourceMysql(db, name, table) 12 | ; 13 | 14 | function add(memo, next){ 15 | async.eachOfSeries(memo.data.ngrams, (ngrams, n, nextN) => { 16 | if(parseInt(n) > 2)return setImmediate(nextN); 17 | 18 | var normalized = {}; 19 | _.forOwn(ngrams, (v, k) => { 20 | var l = k.toLowerCase(); 21 | normalized[l] = v; 22 | }); 23 | 24 | async.eachOfSeries(normalized, (occurances, ngram, nextNgram) => { 25 | if(ngram.length > 64)return setImmediate(nextNgram); 26 | if(ngram.indexOf('"') != -1 || ngram.indexOf("'") == -1)return setImmediate(nextNgram); 27 | if(ngram.indexOf('\\') != -1)return setImmediate(nextNgram); 28 | 29 | datasource.insertOrUpdate(memo.changelog_id, { 30 | values: { 31 | value: ngram, 32 | collection_id: memo.Collection.id, 33 | docs: 1 34 | }, 35 | conditions: { 36 | value: ngram, 37 | collection_id: memo.Collection.id 38 | }, 39 | update: { 40 | docs: (prev, next) => { 41 | return parseInt(prev.docs) + parseInt(next.docs); 42 | } 43 | } 44 | }, (err, insertId) => { 45 | if(err)return setImmediate(() => nextNgram(err)); 46 | setImmediate(nextNgram); 47 | }); 48 | }, (err) => { 49 | setImmediate(() => nextN(err)); 50 | }); 51 | }, (err) => { 52 | console.log('Done ngrams'); 53 | if(err)return setImmediate(() => next(err)); 54 | 55 | setImmediate(() => next(null, memo)); 56 | }); 57 | } 58 | 59 | this.add = add; 60 | 61 | function docsByValue(ngrams, collectionId, callback){ 62 | datasource.find('list', { 63 | fields: ['value', 'docs'], 64 | conditions: { 65 | collection_id: collectionId, 66 | value: ngrams 67 | } 68 | }, callback); 69 | } 70 | 71 | this.docsByValue = docsByValue; 72 | } 73 | -------------------------------------------------------------------------------- /plugin/src/icon.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 53 | 54 | 55 | 56 |

57 | 58 |
59 | 65 |
66 | 67 |

68 | 69 |
70 | 76 |
77 | 78 |

79 | 80 |
81 | 87 |
88 | 89 | 90 | 91 | -------------------------------------------------------------------------------- /webserver/docker/docker-compose.yml: -------------------------------------------------------------------------------- 1 | ## 2 | # 3 | ## 4 | version: '2.1' 5 | 6 | networks: 7 | sf-net: 8 | 9 | services: 10 | mysqlserver: 11 | image: "mysql:5.5" 12 | restart: unless-stopped 13 | environment: 14 | - MYSQL_RANDOM_ROOT_PASSWORD=yes 15 | - MYSQL_DATABASE=storyfinder 16 | - MYSQL_USER=storyfinder 17 | - MYSQL_PORT=3306 18 | - MYSQL_PASSWORD=storyfinder 19 | volumes: 20 | - ${PWD}/mysql-data:/var/lib/mysql 21 | command: ["--character-set-server=utf8", "--collation-server=utf8_bin"] 22 | healthcheck: 23 | test: ["CMD", "mysqladmin" ,"ping", "-h", "localhost", "-pstoryfinder", "-ustoryfinder"] 24 | interval: 20s 25 | timeout: 10s 26 | retries: 3 27 | networks: 28 | sf-net: 29 | aliases: 30 | - mysql 31 | 32 | corenlpserver: 33 | image: "kisad/corenlp-german:latest" 34 | restart: unless-stopped 35 | healthcheck: 36 | test: wget --post-data 'The quick brown fox jumped over the lazy dog.' 'localhost:9000/?properties={"annotators":"tokenize,ssplit,pos,ner","outputFormat":"json"}' -O - 37 | interval: 90s 38 | timeout: 10s 39 | retries: 3 40 | mem_limit: 4g 41 | memswap_limit: 5g 42 | networks: 43 | sf-net: 44 | aliases: 45 | - corenlp 46 | 47 | germanerserver: 48 | image: "kisad/storyfinder-germaner-git:latest" 49 | restart: unless-stopped 50 | stdin_open: true 51 | tty: true 52 | networks: 53 | sf-net: 54 | aliases: 55 | - germaner 56 | 57 | webserver: 58 | image: "uhhlt/storyfinder:latest" 59 | volumes: 60 | - ${PWD}/image-data:/usr/src/app/public/images 61 | ports: 62 | - "3055:3055" 63 | environment: 64 | - NODE_ENV=production 65 | - COOKIE_SECRET=92bls37gfc 66 | - NER=germaner 67 | #- NER=corenlp 68 | - NO_KEYWORDS=T 69 | - CHROME_ID=hnndfanecdfnonofigcceaahflgfpgbd 70 | depends_on: 71 | mysqlserver: 72 | condition: service_healthy 73 | corenlpserver: 74 | condition: service_healthy 75 | restart: unless-stopped 76 | mem_limit: 4g 77 | memswap_limit: 4g 78 | networks: 79 | sf-net: 80 | -------------------------------------------------------------------------------- /webserver/controllers/components/GermanerComponent.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash') 2 | , async = require('async') 3 | , request = require('request') 4 | ; 5 | 6 | module.exports = function(){ 7 | var options = { 8 | host: '127.0.0.1', 9 | port: '8080' 10 | }; 11 | 12 | if(arguments[0]) 13 | options = _.defaults(arguments[0], options); 14 | 15 | function parse(sentences, callback){ 16 | /* 17 | Build GermaNer input format: 18 | sentence1_token1 19 | sentence1_token2 20 | sentence1_token3 21 | . 22 | sentence2_token1 23 | sentence2_token2 24 | ... 25 | */ 26 | var data = []; 27 | 28 | _.each(sentences, function(tokens){ 29 | var sentence = tokens.join("\n"); 30 | 31 | if(sentence.replace(/\s+/g,'').length > 0){ 32 | sentence = sentence.replace(/\n+/g, '\n'); 33 | data.push(sentence); 34 | } 35 | }); 36 | 37 | data = data.join("\n\n") + "\n.\n"; 38 | //console.log(data); 39 | request({ 40 | uri: 'http://' + options.host + ':' + options.port + '/germaner', //ToDo: read from config 41 | method: 'POST', 42 | body: data, 43 | contentType: 'text/plain' 44 | }, (err, response) => { 45 | //console.log('http://' + options.host + ':' + options.port + '/germaner', err, response, body); 46 | if(err)return setImmediate(() => callback(err)); 47 | if(response.statusCode != 200)return setImmediate(() => callback(new Error('Invalid status code ' + response.statusCode + ' of GermaNer (expected: 200)'))); 48 | 49 | /* 50 | The server responses with one entity per line prefixed by the entity type: 51 | TYPE entity1 52 | TYPE entity2 53 | TYPE entity3 54 | */ 55 | 56 | var entities = []; 57 | response.body.split('\n').forEach(entity => { 58 | entity = entity.replace(/^\s+/g,'').replace(/\s+$/g,''); //Remove surrounding spaces 59 | 60 | if(entity.length > 0){ 61 | var tokens = entity.split(/\s+/g) 62 | , type = tokens.shift() 63 | , value = tokens.join(' ') 64 | ; 65 | 66 | entities.push({ 67 | type: type, 68 | value: value 69 | }); 70 | } 71 | }); 72 | 73 | setImmediate(() => callback(null, entities)); 74 | }); 75 | } 76 | 77 | this.parse = parse; 78 | } -------------------------------------------------------------------------------- /webserver/public/register.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Storyfinder 8 | 11 | 12 | 13 |
14 |
15 |
16 |
17 | 23 |
24 | 25 | 26 | 27 |
28 |

StoryFinder

29 |
30 |
31 |
32 | 33 | 34 | 35 |
36 |
37 | 38 | 39 | 40 |
41 |
42 | 43 | 44 | 45 |
46 | 47 | 48 |
49 |

50 | Already have an account?
Click here to login with your username and password. 51 |

52 |
53 |
54 |
55 |
56 | 57 | 58 | -------------------------------------------------------------------------------- /webserver/public/js/templates/relations/relation.hbs: -------------------------------------------------------------------------------- 1 |
2 | arrow_back 3 |
{{Entities.[0].caption}}
4 |
5 |
6 | {{#if Relation}} 7 |
8 |
keyboard_arrow_up
9 |
10 |
keyboard_arrow_down
11 |
12 |
    13 |
  • 14 |
    15 | 16 |
    17 | 18 |
    19 |
    20 |
  • 21 | {{#if Sentences}} 22 | {{#each Sentences}} 23 |
  • 24 |
    »{{{html}}} «
    25 |
    – Source {{Article.Site.title}} visited on {{Article.Site.last_visited}} –
    26 |
    27 | 28 |
    29 | 30 |
    31 |
    32 |
  • 33 | {{/each}} 34 | {{else}} 35 |
  • 36 |
    There is currently no source for this relation.
    37 |
  • 38 | {{/if}} 39 |
40 | {{else}} 41 |
42 | There is currently no relation between »{{Entities.[0].caption}}« and »{{Entities.[1].caption}}«.
Do you want to create a new relation? 43 |

44 |

45 | 46 |
47 | 48 |
49 | Yes, create relation 50 | No 51 |
52 |
53 | {{/if}} 54 |
55 | -------------------------------------------------------------------------------- /webserver/controllers/components/KeywordComponent.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash') 2 | , async = require('async') 3 | , fs = require('fs') 4 | , path = require('path') 5 | ; 6 | 7 | module.exports = function(Ngram, Article){ 8 | var t = { 9 | 0: 3, 10 | 1: 3, 11 | 2: 3 12 | }; 13 | 14 | function getKeywords(candidates, nsize, collectionId, callback){ 15 | Article.countDocumentsInCollection(collectionId, (err, d) => { 16 | if(err)return setImmediate(() => callback(err)); 17 | d++; //Add +1 for the current document 18 | 19 | if(d < 4){ 20 | console.log('Keyword extraction expects at least 3 documents in the corpus'); 21 | return setImmediate(() => callback(null, [])); 22 | } 23 | 24 | var weights = []; 25 | 26 | Ngram.docsByValue(_.keys(candidates), collectionId, (err, docs) => { 27 | if(err)return setImmediate(() => callback(err)); 28 | 29 | //Get the maximum term frequency for normalization 30 | var tfMax = _.reduce(candidates, (n, o) => { 31 | return Math.max(n, o.length); 32 | }, 1); 33 | 34 | //Calculate weights 35 | _.forOwn(candidates, (o, candidate) => { 36 | var n = 0; 37 | if(docs[candidate]) 38 | n = docs[candidate].docs; 39 | n++; 40 | 41 | var tf = o.length / tfMax 42 | ; 43 | 44 | /* 45 | d = #documents 46 | n = #documents containg candidate 47 | tf = Normalized termfrequency of candidate 48 | */ 49 | 50 | var idf = Math.log(d/n); 51 | if(o.caption.match(/^[A-Za-z0-9\s]+$/)) 52 | weights.push({w: tf * idf, ngram: o.caption, idf: idf, n: n, tf: tf}); 53 | }); 54 | 55 | //Sort weights 56 | weights.sort((a, b) => { 57 | return b.w - a.w; 58 | }); 59 | 60 | /* 61 | Only extract elements with a value > t[n] 62 | */ 63 | 64 | console.log(weights.slice(0, 10)); 65 | 66 | var keywords = []; 67 | 68 | //Use the top 3 elements as keywords 69 | for(var c of weights){ 70 | if(_.isUndefined(t[nsize]))break; 71 | if(c.w < t[nsize])break; 72 | 73 | keywords.push(c.ngram); 74 | } 75 | 76 | /*weights.slice(0, 3).forEach((c) => { 77 | keywords.push(c.ngram); 78 | }); */ 79 | 80 | /*var c = weights.shift() 81 | keywords = [] 82 | ; 83 | 84 | while(c.w > t){ 85 | keywords.push(c.ngram); 86 | 87 | if(weights.length == 0)break; 88 | c = weights.shift(); 89 | }*/ 90 | 91 | console.log('Keywords', keywords); 92 | 93 | setImmediate(() => callback(null, keywords)); 94 | }); 95 | }); 96 | } 97 | 98 | this.getKeywords = getKeywords; 99 | } -------------------------------------------------------------------------------- /webserver/README.md: -------------------------------------------------------------------------------- 1 | # Instructions to start the server 2 | 3 | 1. Create dockers 4 | 5 | only for dev-server: 6 | 2. Attach to docker 7 | 3. Build the server with npm run build 8 | 4. npm start 9 | 10 | 5. Server is now accessible at http://localhost:3055/ 11 | 12 | # Docker 13 | 14 | ### Choose docker-compose file: 15 | docker-compose-dev: Development Version, uses CHROME_ID = pebdjeaapfkjiceloeecpoedbliefnap 16 | 17 | docker-compose: Live Version, uses CHROME_ID = hnndfanecdfnonofigcceaahflgfpgbd 18 | 19 | ### Create dockers 20 | 21 | ``` 22 | $ cd storyfinder/webserver/docker 23 | $ docker-compose -f docker-compose-dev.yml -p storyfinder up -d 24 | ``` 25 | 26 | ### Attach docker 27 | 28 | ``` 29 | $ docker attach storyfinder_webserver-dev_1 30 | ``` 31 | 32 | ### Delete dockers 33 | 34 | ``` 35 | $ cd storyfinder/webserver/docker 36 | $ docker-compose -f docker-compose-dev.yml -p storyfinder down 37 | ``` 38 | 39 | # Recommended workflow 40 | 41 | 1. make changes to the code 42 | 2. automatically build the server with npm run build 43 | 3. automatically publish the server to dockerhub with npm run publish 44 | 45 | # Build the server 46 | 47 | ## Automatic build 48 | this installs the npm modules and builds storyfinder.js and register.js 49 | 50 | ``` 51 | $ cd storyfinder/webserver 52 | $ npm run build 53 | ``` 54 | 55 | ## Manual build 56 | 57 | ### Manually install npm modules 58 | 59 | ``` 60 | $ npm install -g browserify 61 | 62 | $ cd /storyfinder/webserver 63 | $ npm install 64 | 65 | $ cd /storyfinder/webserver/public/js 66 | $ npm install 67 | ``` 68 | 69 | ### Manually build storyfinder.js 70 | 71 | ``` 72 | $ cd /storyfinder/webserver/public/js 73 | $ browserify index.js -t babelify -t [hbsfy -t] -o storyfinder.js 74 | ``` 75 | 76 | ### Manually build register.js 77 | 78 | ``` 79 | $ cd /storyfinder/webserver/public/js 80 | $ browserify register/register.js -t babelify -t [hbsfy -t] -o register.js 81 | ``` 82 | 83 | # Publish the server 84 | 85 | ## Automatic publish 86 | this automatically builds a the storyfinder-docker and pushes it to uhhlt/storyfinder:latest 87 | 88 | ``` 89 | $ cd storyfinder/webserver 90 | $ npm run publish 91 | ``` 92 | 93 | ## Manual publish 94 | 95 | ### Set the version 96 | 97 | ``` 98 | $ v=0.0.10 99 | ``` 100 | 101 | ### Build the storyfinder-docker 102 | 103 | ``` 104 | $ cd storyfinder/webserver 105 | $ docker login --username= 106 | $ docker build -t uhhlt/storyfinder:$v . 107 | ``` 108 | 109 | ### Push to uhhlt/storyfinder 110 | 111 | ``` 112 | $ docker login --username= 113 | $ docker push uhhlt/storyfinder:$v 114 | ``` 115 | -------------------------------------------------------------------------------- /webserver/docker/docker-compose-dev.yml: -------------------------------------------------------------------------------- 1 | # 2 | # docker-compose -p storyfinder-dev -f docker-compose-dev.yml up 3 | # 4 | # or 5 | # docker-compose -p storyfinder-dev -f docker-compose-dev.yml up --build 6 | # docker-compose -p storyfinder-dev -f docker-compose-dev.yml up -d 7 | # 8 | # docker-compose -p storyfinder-dev -f docker-compose-dev.yml down 9 | # 10 | version: '2.1' 11 | 12 | networks: 13 | sf-net: 14 | 15 | services: 16 | mysqlserver: 17 | image: "mysql:5.5" 18 | environment: 19 | - MYSQL_RANDOM_ROOT_PASSWORD=yes 20 | - MYSQL_DATABASE=storyfinder 21 | - MYSQL_USER=storyfinder 22 | - MYSQL_PORT=3306 23 | - MYSQL_PASSWORD=storyfinder 24 | volumes: 25 | - ${PWD}/mysql-data-dev:/var/lib/mysql 26 | command: ["--character-set-server=utf8", "--collation-server=utf8_bin"] 27 | healthcheck: 28 | test: ["CMD", "mysqladmin" ,"ping", "-h", "localhost", "-pstoryfinder", "-ustoryfinder"] 29 | interval: 20s 30 | timeout: 10s 31 | retries: 3 32 | networks: 33 | sf-net: 34 | aliases: 35 | - mysql 36 | 37 | corenlpserver: 38 | image: "kisad/corenlp-german:latest" 39 | # healthcheck: 40 | # test: wget --post-data 'The quick brown fox jumped over the lazy dog.' 'localhost:9000/?properties={"annotators":"tokenize,ssplit,pos,ner","outputFormat":"json"}' -O - 41 | # interval: 20s 42 | # timeout: 10s 43 | # retries: 3 44 | # mem_limit: 4g 45 | # memswap_limit: 5g 46 | networks: 47 | sf-net: 48 | aliases: 49 | - corenlp 50 | 51 | germanerserver: 52 | image: "kisad/storyfinder-germaner-git:latest" 53 | #image: "kisad/storyfinder-germaner-git:latest" 54 | #healthcheck: 55 | # test: curl -X POST -H 'Content-Type: text/plain' -d 'Angela^MMerkel^Mbesucht^MFrankreich^M.' 'localhost:8080/germaner' 56 | # interval: 20s 57 | # timeout: 10s 58 | # retries: 3 59 | #mem_limit: 4g 60 | #memswap_limit: 5g 61 | stdin_open: true 62 | tty: true 63 | networks: 64 | sf-net: 65 | aliases: 66 | - germaner 67 | 68 | webserver-dev: 69 | depends_on: 70 | mysqlserver: 71 | condition: service_healthy 72 | # corenlpserver: 73 | # condition: service_healthy 74 | build: 75 | context: ./ 76 | dockerfile: Dockerfile-dev 77 | ports: 78 | - "3056:3055" 79 | volumes: 80 | - ../:/usr/src/app 81 | stdin_open: true 82 | tty: true 83 | working_dir: /usr/src/app 84 | entrypoint: bash 85 | environment: 86 | - NODE_ENV=development 87 | - COOKIE_SECRET=YOUR_COOKIE_SECRET 88 | - NER=corenlp 89 | # - NER=germaner 90 | - NO_KEYWORDS=true 91 | - CHROME_ID=pebdjeaapfkjiceloeecpoedbliefnap 92 | mem_limit: 4g 93 | memswap_limit: 5g 94 | networks: 95 | sf-net: 96 | -------------------------------------------------------------------------------- /webserver/models/User.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash') 2 | , async = require('async') 3 | , bcrypt = require('bcrypt') 4 | ; 5 | 6 | module.exports = function(db){ 7 | function create(data, callback){ 8 | var date = (new Date()).toISOString().slice(0,19).replace('T',' '); 9 | 10 | bcrypt.hash(data.password, 5, function(err, hash) { 11 | if(err) 12 | return setImmediate(() => { 13 | callback(err); 14 | }); 15 | 16 | data.password = hash; 17 | 18 | db.query('INSERT INTO `users` (username, password, created, modified, is_active, is_deleted) VALUES (?, ?, ?, ?, 1, 0)', [ 19 | data.username, 20 | hash, 21 | date, 22 | date 23 | ], function(err, res){ 24 | if(err) 25 | return setImmediate(() => { 26 | callback(err); 27 | }); 28 | 29 | setImmediate(() => { 30 | callback(null, { 31 | id: res.insertId 32 | }); 33 | }); 34 | }); 35 | }); 36 | } 37 | 38 | this.create = create; 39 | 40 | function exists(username, callback){ 41 | db.query('SELECT count(*) `cnt` FROM `users` WHERE username=? LIMIT 1', [ 42 | username 43 | ], function(err, res2){ 44 | if(err) 45 | return setImmediate(() => { 46 | callback(err); 47 | }); 48 | 49 | console.log('User exists?' + res2[0]['cnt']); 50 | 51 | setImmediate(() => { 52 | callback(null, res2[0]['cnt']?true:false); 53 | }); 54 | }); 55 | } 56 | 57 | this.exists = exists; 58 | 59 | function verify(username, suppliedPassword, callback){ 60 | console.log('Verify'); 61 | db.query('SELECT * FROM `users` WHERE username=? LIMIT 1', [username], function(err, results, fields){ 62 | if(err) 63 | return setImmediate(() => { 64 | callback(err); 65 | }); 66 | 67 | if(results == null || results.length == 0) 68 | return setImmediate(() => { 69 | callback(null, false); 70 | }); 71 | 72 | var user = results[0]; 73 | 74 | if(!user.is_active) 75 | return setImmediate(() => { 76 | callback(null, false); 77 | }); 78 | 79 | bcrypt.compare(suppliedPassword, user.password, function(err, doesMatch){ 80 | if (doesMatch){ 81 | return setImmediate(() => { 82 | callback(null, true, user); 83 | }); 84 | } 85 | 86 | setImmediate(() => { 87 | callback(null, false); 88 | }); 89 | }); 90 | }); 91 | } 92 | 93 | this.verify = verify; 94 | 95 | function findById(id, callback){ 96 | db.query('SELECT * FROM `users` WHERE id="?" LIMIT 1', [id], function(err, results, fields){ 97 | if(err) 98 | return setImmediate(() => { 99 | callback(err); 100 | }); 101 | 102 | if(results == null || results.length == 0) 103 | return setImmediate(() => { 104 | callback(null, null); 105 | }); 106 | 107 | var user = results[0]; 108 | 109 | if(!user.is_active) 110 | return setImmediate(() => { 111 | callback(null, null); 112 | }); 113 | 114 | setImmediate(() => { 115 | callback(null, user); 116 | }); 117 | }); 118 | } 119 | 120 | this.findById = findById; 121 | }; -------------------------------------------------------------------------------- /webserver/public/js/vis/transitions/removeDeleted.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash') 2 | , async = require('async') 3 | ; 4 | 5 | module.exports = function(options, renderGraph, node, label, link){ 6 | function getScalingFactor(d){ 7 | if(!_.isUndefined(d.focused) && d.focused == true && !_.isUndefined(d.tfidf) && !_.isNaN(d.tfidf)) 8 | return d.tfidf; 9 | return d.pageRank; 10 | } 11 | 12 | /* 13 | Geloeschte Elemente im Graphen ausblenden 14 | */ 15 | function removeDeleted(){ 16 | var done = arguments[arguments.length - 1]; 17 | var srcNode = null 18 | , srcData = null 19 | , srcId = null 20 | ; 21 | 22 | if(arguments.length > 1){ 23 | srcId = arguments[0]; 24 | 25 | srcNode = label.filter(function(d){ 26 | if(d.id == srcId)return true; 27 | return false; 28 | }); 29 | srcData = srcNode.datum(); 30 | } 31 | 32 | if(_.isNull(node) || _.isNull(label) || _.isNull(link)){ 33 | done(); 34 | }else{ 35 | var removeA = {}; 36 | if(!_.isUndefined(renderGraph.remove)) 37 | renderGraph.remove.forEach(function(id){ 38 | removeA[id] = true; 39 | }); 40 | 41 | //Alte Nodes und Links entfernen 42 | var nodesToRemove = node.filter(function(d){ 43 | return !_.isUndefined(removeA[d.id]); 44 | }); 45 | 46 | var labelsToRemove = label.filter(function(d){ 47 | return !_.isUndefined(removeA[d.id]); 48 | }); 49 | 50 | var linksToRemove = link.filter(function(d){ 51 | return !_.isUndefined(removeA[d.source.id]) || !_.isUndefined(removeA[d.target.id]); 52 | }); 53 | 54 | async.parallel([ 55 | function(doneTransition){ 56 | var n = 0; 57 | var transition = labelsToRemove 58 | .transition() 59 | .duration(options.transitionRemove) 60 | .attrTween('transform', function(d, i, a){ 61 | var x = [d.x, -1 * d.width + 2] 62 | , y = [d.y, d.y] 63 | ; 64 | 65 | if(!_.isNull(srcData)){ 66 | x[1] = srcData.x; 67 | y[1] = srcData.y; 68 | } 69 | 70 | return function(t){ 71 | var ret = 'translate(' + Math.round(x[0] * (1 - t) + x[1] * t) + ',' + Math.round(y[0] * (1 - t) + y[1] * t) + ') scale(' + (getScalingFactor(d) / 2 + 0.75) + ')'; 72 | return ret; 73 | } 74 | }) 75 | .attr('opacity', _.isNull(srcData)?1:0) 76 | .each('end', function(){ 77 | if(transition.size() == ++n) 78 | doneTransition() 79 | }); 80 | 81 | if(transition.size() == 0) 82 | doneTransition(); 83 | }, 84 | function(doneTransition){ 85 | var n = 0; 86 | var transition = linksToRemove 87 | .transition() 88 | .duration(options.transitionRemove) 89 | .attr('opacity', 0) 90 | .each('end', function(){ 91 | if(transition.size() == ++n) 92 | doneTransition() 93 | }); 94 | 95 | if(transition.size() == 0) 96 | doneTransition(); 97 | } 98 | ], function(){ 99 | console.log('Done removing'); 100 | setTimeout(done, 0); 101 | }); 102 | } 103 | } 104 | 105 | this.removeDeleted = removeDeleted; 106 | } -------------------------------------------------------------------------------- /webserver/controllers/components/CorenlpComponent.js: -------------------------------------------------------------------------------- 1 | var request = require('request') 2 | , _ = require('lodash') 3 | , async = require('async') 4 | ; 5 | 6 | module.exports = function(){ 7 | var options = _.defaults(arguments[0], { 8 | host: 'localhost', 9 | port: '9000' 10 | }); 11 | 12 | var defaultProperties = {}; 13 | if(typeof arguments[1] != 'undefined' && arguments[1] != null) 14 | defaultProperties = _.defaults(arguments[1], defaultProperties); 15 | 16 | function parse(data){ 17 | var properties = {}; 18 | 19 | if(arguments.length == 3) 20 | properties = arguments[1]; 21 | 22 | properties = _.defaults(properties, defaultProperties); 23 | 24 | var callback = arguments[arguments.length - 1]; 25 | 26 | var paragraphs = data.split(/(\n\s*\n)+/g); 27 | 28 | async.mapLimit(paragraphs, 5, (paragraph, nextParagraph) => { 29 | //Skip paragraphs which look too strange 30 | var ratio = 1 / paragraph.length * (paragraph.length - paragraph.replace(/[A-Za-z\-\,\.\?\s]/g,'').length); 31 | var avgWordLength = paragraph.length / paragraph.split(/\s+/g).length; 32 | //console.log('Parsing: ' + paragraph + "\n********\nRatio: " + ratio + "\n" + avgWordLength + "\n*******"); 33 | 34 | if(ratio < 0.9 || avgWordLength > 10){ 35 | console.log('Skipping paragraph (Ratio: ' + ratio + ', avg. word length: ' + avgWordLength + ': ' + paragraph.replace(/\s+/g,' ')); 36 | return setImmediate(() => nextParagraph(null, null)); 37 | } 38 | 39 | request({ 40 | uri: 'http://' + options.host + ':' + options.port + '/?properties=' + encodeURIComponent(JSON.stringify(properties)), 41 | method: 'POST', 42 | body: paragraph, 43 | contentType: 'text/plain' 44 | }, function (error, response, body) { 45 | 46 | body = body.replace(/\\n/g, "\\n") 47 | .replace(/\\'/g, "\\'") 48 | .replace(/\\"/g, '\\"') 49 | .replace(/\\&/g, "\\&") 50 | .replace(/\\r/g, "\\r") 51 | .replace(/\\t/g, "\\t") 52 | .replace(/\\b/g, "\\b") 53 | .replace(/\\f/g, "\\f") 54 | .replace(/[\u0000-\u0019]+/g,"") 55 | ; 56 | 57 | if (!error && response.statusCode == 200) { 58 | setImmediate(function(){ 59 | var json = null; 60 | try { 61 | json = JSON.parse(body); 62 | }catch(e){ 63 | json = null; 64 | } 65 | nextParagraph(null, json); 66 | }); 67 | }else if(error){ 68 | nextParagraph(null, error); 69 | }else{ 70 | nextParagraph(null, new Error('Statuscode: ' + response.statusCode + "\n" + body)); 71 | } 72 | }); 73 | }, (err, parsed) => { 74 | if(err) 75 | return setImmediate(() => callback(err)); 76 | 77 | var sentences = []; 78 | 79 | parsed.forEach((p) => { 80 | if(_.isError(p))return; 81 | if(_.isNull(p))return; 82 | if(_.isEmpty(p.sentences))return; 83 | 84 | p.sentences.forEach((s) => { 85 | sentences.push(s); 86 | }); 87 | }); 88 | //return setImmediate(() => callback(new Error('break'))); 89 | setImmediate(() => callback(null, {sentences: sentences})); 90 | }); 91 | } 92 | 93 | this.parse = parse; 94 | }; -------------------------------------------------------------------------------- /webserver/models/EntitySentence.js: -------------------------------------------------------------------------------- 1 | var DatasourceMysql = require('../datasources/mysql.js') 2 | , async = require('async') 3 | , _ = require('lodash') 4 | ; 5 | 6 | module.exports = function(db){ 7 | var name = 'EntitySentence' 8 | , models = _.defaults({EntitySentence: this}, arguments[1] || {}) 9 | , table = 'entities_sentences' 10 | , datasource = new DatasourceMysql(db, name, table) 11 | ; 12 | 13 | function findBySentence(/*sentenceIds,[ options,] callback*/){ 14 | var sentenceIds = arguments[0] 15 | , options = { 16 | exclude: null 17 | } 18 | , callback = arguments[arguments.length - 1] 19 | ; 20 | 21 | if(arguments.length > 2) 22 | options = _.defaults(arguments[1], options); 23 | 24 | var conditions = { 25 | sentence_id: sentenceIds, 26 | is_deleted: 0 27 | }; 28 | 29 | if(!_.isNull(options.exclude)) 30 | conditions['entity_id <>'] = options.exclude; 31 | 32 | datasource.find('all', { 33 | fields: ['id', 'sentence_id', 'entity_id'], 34 | conditions: conditions 35 | }, (err, results) => { 36 | if(err)return setImmediate(() => callback(err)); 37 | if(_.isEmpty(results))return setImmediate(() => callback(null, {})); 38 | 39 | var values = {}; 40 | results.forEach(r => (values[r.entity_id] || (values[r.entity_id] = [])).push(r.sentence_id)); 41 | 42 | setImmediate(() => callback(null, values)); 43 | }); 44 | } 45 | 46 | this.findBySentence = findBySentence; 47 | 48 | function add(memo, next){ 49 | async.eachOfSeries(memo.data.Entities, (entity, key, nextEntity) => { 50 | 51 | //Sentences of entity 52 | if(_.isUndefined(memo.data.ngrams[entity.tokens - 1]) || _.isUndefined(memo.data.ngrams[entity.tokens - 1][entity.caption])) 53 | return setImmediate(nextEntity); 54 | 55 | async.eachSeries(memo.data.ngrams[entity.tokens - 1][entity.caption], (o, nextSentence) => { 56 | var sentenceId = memo.data.sentences[o.sentence].id; 57 | 58 | datasource.insertOrUpdate(memo.changelog_id, { 59 | values: { 60 | sentence_id: sentenceId, 61 | entity_id: entity.id, 62 | is_deleted: 0 63 | }, 64 | conditions: { 65 | sentence_id: sentenceId, 66 | entity_id: entity.id 67 | } 68 | }, (err, insertId) => { 69 | if(err)return setImmediate(() => nextEntity(err)); 70 | 71 | setImmediate(nextSentence); 72 | }); 73 | }, nextEntity); 74 | }, (err) => { 75 | if(err)return setImmediate(() => next(err)); 76 | 77 | setImmediate(() => next(null, memo)); 78 | }); 79 | } 80 | 81 | this.add = add; 82 | 83 | function addByEntity(memo, next){ 84 | async.eachSeries(memo.Sentences, (sentence, nextSentence) => { 85 | var sentenceId = sentence.id; 86 | 87 | datasource.insertOrUpdate(memo.changelog_id, { 88 | values: { 89 | sentence_id: sentenceId, 90 | entity_id: memo.Entity.id, 91 | is_deleted: 0 92 | }, 93 | conditions: { 94 | sentence_id: sentenceId, 95 | entity_id: memo.Entity.id 96 | } 97 | }, (err, insertId) => { 98 | if(err)return setImmediate(() => nextEntity(err)); 99 | 100 | setImmediate(nextSentence); 101 | }); 102 | }, (err) => { 103 | if(err)return setImmediate(() => next(err)); 104 | 105 | setImmediate(() => next(null, memo)); 106 | }); 107 | } 108 | 109 | this.addByEntity = addByEntity; 110 | } 111 | -------------------------------------------------------------------------------- /webserver/controllers/UsersController.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash') 2 | , async = require('async') 3 | , fs = require('fs') 4 | ; 5 | 6 | module.exports = function(connection, app, passport){ 7 | var User = new (require('../models/User.js'))(connection) 8 | ; 9 | 10 | /* 11 | Register 12 | */ 13 | app.put('/Users', function(req, res){ 14 | if(_.isEmpty(req.body) || _.isEmpty(req.body.username) || _.isEmpty(req.body.password)){ 15 | return setImmediate(() => { 16 | res.sendStatus(400); 17 | }); 18 | 19 | return; 20 | } 21 | 22 | User.exists(req.body.username, (err, userexists) => { 23 | if(err){ 24 | console.log(err); 25 | return setImmediate(() => { 26 | res.sendStatus(500); 27 | }); 28 | } 29 | 30 | if(userexists) 31 | return setImmediate(() => { 32 | res.send({ 33 | success: false, 34 | message: 'An account for the given Email address exists already!' 35 | }); 36 | }); 37 | console.log("User exists? "+userexists); 38 | 39 | User.create(req.body, (err, user) => { 40 | if(err){ 41 | console.log(err); 42 | return setImmediate(() => { 43 | res.sendStatus(500); 44 | }); 45 | } 46 | 47 | var userId = user.id; 48 | 49 | User.findById(userId, (err, user) => { 50 | if(err){ 51 | console.log(err); 52 | return setImmediate(() => { 53 | res.sendStatus(500); 54 | }); 55 | } 56 | 57 | 58 | req.login(user, function(err) { 59 | if(err){ 60 | console.log(err); 61 | return setImmediate(() => { 62 | res.sendStatus(500); 63 | }); 64 | } 65 | 66 | res.send({ 67 | success: true, 68 | id: userId 69 | }); 70 | }); 71 | }); 72 | }); 73 | }); 74 | }); 75 | 76 | /* 77 | Show registration form 78 | */ 79 | app.get('/register', function (req, res) { 80 | fs.readFile(__dirname + '/../public/css/bootstrap.css', function(err, style){ 81 | if(err){ 82 | console.log(err); 83 | return; 84 | } 85 | fs.readFile(__dirname + '/../public/register.html', function(err, html){ 86 | if(err){ 87 | console.log(err); 88 | return; 89 | } 90 | res.send(html.toString().replace(/\{\{style\}\}/, style.toString())); 91 | }); 92 | }); 93 | }); 94 | 95 | /* 96 | Show login form 97 | */ 98 | app.get('/login', function (req, res) { 99 | fs.readFile(__dirname + '/../public/css/bootstrap.css', function(err, style){ 100 | if(err){ 101 | console.log(err); 102 | return; 103 | } 104 | fs.readFile(__dirname + '/../public/login.html', function(err, html){ 105 | if(err){ 106 | console.log(err); 107 | return; 108 | } 109 | res.send(html.toString().replace(/\{\{style\}\}/, style.toString())); 110 | }); 111 | }); 112 | }); 113 | 114 | /* 115 | Login 116 | */ 117 | 118 | app.post('/login', 119 | passport.authenticate('local', { successRedirect: (process.env.PATH_PREFIX || '/') + 'Users/status', failureRedirect: (process.env.PATH_PREFIX || '/') + 'error404' })); 120 | 121 | /* 122 | Return login status 123 | */ 124 | app.get('/Users/status', function (req, res, next) { 125 | console.log(req.isAuthenticated()); 126 | if (!req.isAuthenticated || !req.isAuthenticated()) { 127 | passport.authenticate('basic', {session: true})(req, res, next); 128 | }else{ 129 | next(); 130 | } 131 | }, function(req, res){ 132 | res.send({ 133 | success: true 134 | }); 135 | }); 136 | 137 | /* 138 | Logout 139 | */ 140 | app.get('/logout', function(req, res){ 141 | req.logout(); 142 | res.redirect((process.env.PATH_PREFIX || '/')); 143 | }); 144 | }; 145 | -------------------------------------------------------------------------------- /plugin/src/menu.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Storyfinder 6 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | -------------------------------------------------------------------------------- /webserver/public/js/vis/transitions/showNew.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash') 2 | , async = require('async') 3 | , smoothLine = require('../helpers/smoothLine.js') 4 | , d3 = require('d3') 5 | ; 6 | 7 | module.exports = function(options, elNew, elExisting, renderGraph, node, label, link){ 8 | function getScalingFactor(d){ 9 | if(!_.isUndefined(d.focused) && d.focused == true && !_.isUndefined(d.tfidf) && !_.isNaN(d.tfidf)) 10 | return d.tfidf; 11 | return d.pageRank; 12 | } 13 | 14 | /* 15 | Neue Knoten und links einblenden 16 | */ 17 | function showNew(){ 18 | var done = arguments[arguments.length - 1]; 19 | var srcNode = null 20 | , srcData = null 21 | , srcId = null 22 | ; 23 | 24 | if(arguments.length > 1){ 25 | srcId = arguments[0]; 26 | 27 | srcNode = label.filter(function(d){ 28 | if(d.id == srcId)return true; 29 | return false; 30 | }); 31 | srcData = srcNode.datum(); 32 | } 33 | 34 | async.parallel([ 35 | function(doneTransition){ 36 | var n = 0; 37 | var transition = elNew.labels 38 | .attr('transform', function(d){ 39 | if(_.isNull(srcData)) 40 | return 'translate(' + (options.width + d.width) + ',' + Math.round(d.y) + ') scale(' + (getScalingFactor(d) / 2 + 0.75) + ')'; 41 | else 42 | return 'translate(' + srcData.x + ',' + srcData.y + ') scale(' + 0 + ')'; 43 | }) 44 | .transition() 45 | .duration(options.transitionAdd) 46 | .delay(function(d, i){ 47 | if(_.isNull(srcNode))return 0; 48 | return i * 50; 49 | }) 50 | .ease('bounce') 51 | .attrTween('transform', function(d, i, a){ 52 | var x = [options.width + d.width, d.x] 53 | , y = [d.y, d.y] 54 | , p = [(getScalingFactor(d) / 2 + 0.75), (getScalingFactor(d) / 2 + 0.75)] 55 | ; 56 | 57 | if(!_.isNull(srcData)){ 58 | x[0] = srcData.x; 59 | y[0] = srcData.y; 60 | p[0] = 0; 61 | } 62 | 63 | return function(t){ 64 | var ret = 'translate(' + Math.round(x[0] * (1 - t) + x[1] * t) + ',' + Math.round(y[0] * (1 - t) + y[1] * t) + ') scale(' + (Math.round(p[0] * (1 - t) + p[1] * t)) + ')'; 65 | return ret; 66 | } 67 | }) 68 | .attr('opacity', 1) 69 | .each('end', function(){ 70 | if(transition.size() == ++n) 71 | doneTransition(); 72 | }); 73 | 74 | if(transition.size() == 0) 75 | doneTransition(); 76 | }, 77 | function(doneTransition){ 78 | var n = 0; 79 | var transition = elNew.nodes 80 | .attr('transform', function(d){ 81 | return 'translate(' + Math.round(d.x) + ',' + Math.round(d.y) + ') scale(' + (getScalingFactor(d) / 2 + 0.75) + ')'; 82 | }) 83 | .transition() 84 | .duration(options.transitionAdd) 85 | .attr('opacity', 1) 86 | .each('end', function(){ 87 | if(transition.size() == ++n) 88 | doneTransition(); 89 | }); 90 | 91 | if(transition.size() == 0) 92 | doneTransition(); 93 | }, 94 | function(doneTransition){ 95 | var n = 0; 96 | elNew.links 97 | .selectAll('path') 98 | .attr('d', function(d){ 99 | var sx = d.source.x 100 | , sy = d.source.y 101 | , tx = d.target.x 102 | , ty = d.target.y 103 | ; 104 | 105 | return smoothLine({x: sx, y: sy}, {x:tx, y:ty}); 106 | }); 107 | 108 | var transition = elNew.links 109 | .attr('opacity', 0) 110 | .transition() 111 | .delay(function(d, i){ 112 | if(_.isNull(srcNode))return 0; 113 | return i * 50; 114 | }) 115 | .duration(options.transitionAdd) 116 | .attr('opacity', 1) 117 | .each('end', function(){ 118 | if(transition.size() == ++n) 119 | doneTransition(); 120 | }); 121 | 122 | if(transition.size() == 0) 123 | doneTransition(); 124 | } 125 | ], done); 126 | } 127 | 128 | this.showNew = showNew; 129 | } -------------------------------------------------------------------------------- /webserver/controllers/components/RandomforestComponent.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs') 2 | , _ = require('lodash') 3 | ; 4 | 5 | module.exports = function(treefile){ 6 | if(!fs.existsSync(treefile)) 7 | throw new Error('Treefile not found: ' + treefile); 8 | 9 | var trees = fs.readFileSync(treefile).toString().split('\n---\n'); 10 | 11 | /* 12 | Parse the treefile and create an object for every tree 13 | */ 14 | function parseTrees(){ 15 | console.log('Parsing trees in ' + treefile); 16 | for(var t in trees){ 17 | trees[t] = parseTree(trees[t]); 18 | } 19 | console.log('Done parsing trees'); 20 | } 21 | 22 | function parseTree(tree){ 23 | var lvl = 0; 24 | var rows = tree.split("\n"); 25 | var parsed = []; 26 | 27 | for(var row of rows){ 28 | var m = row.match(/(\| )+/g); 29 | if(m === null) 30 | lvl = 0; 31 | else{ 32 | lvl = m[0].length / 4; 33 | row = row.substr(m[0].length); 34 | } 35 | 36 | var className = null; 37 | var leaf = row.match(/ : (\d+) \(\d+\/\d+\)$/); 38 | if(leaf !== null){ 39 | //Leaf 40 | className = parseInt(leaf[1]); 41 | row = row.substr(0, row.length - leaf[0].length); 42 | } 43 | 44 | var condition = row.match(/^([a-zA-Z0-9\-\_]+) ([\<\>\=]+) ([\-]?\d+(\.\d+)?)/); 45 | var el = { 46 | key: condition[1], 47 | relation: condition[2], 48 | value: parseFloat(condition[3]) 49 | }; 50 | 51 | if(className !== null){ 52 | el.className = className; 53 | }else 54 | el.children = []; 55 | 56 | var tgt = parsed; 57 | for(var l = 0; l < lvl; l++) 58 | tgt = tgt[tgt.length - 1].children; 59 | tgt.push(el); 60 | } 61 | 62 | return parsed; 63 | } 64 | 65 | /* 66 | Classify the given data by testing all trees and using the maximum predicted class 67 | */ 68 | function classify(data){ 69 | var classes = {}; 70 | for(var tree of trees){ 71 | c = test(tree, data); 72 | 73 | if(_.isUndefined(classes[c])) 74 | classes[c] = { 75 | classname: c, 76 | votes: 0 77 | }; 78 | 79 | classes[c].votes++; 80 | 81 | console.log(classes); 82 | } 83 | 84 | classes = _.values(classes); 85 | 86 | classes.sort((a, b) => { 87 | return a.votes - b.votes; 88 | }); 89 | 90 | //console.log(classes); 91 | 92 | return classes[0].classname; 93 | } 94 | 95 | /* 96 | Classify the data with the given tree 97 | */ 98 | function test(tree, data){ 99 | var conditions = _.clone(tree); 100 | 101 | do{ 102 | var hasMatch = false; 103 | 104 | for(var condition of conditions){ 105 | if(typeof data[condition.key] === 'undefined'){ 106 | console.log('Data has no key ' + condition.key, data); 107 | continue; 108 | } 109 | 110 | switch(condition.relation){ 111 | case '<': 112 | //console.log(data[condition.key] + ' < ' + condition.value); 113 | if(!(data[condition.key] < condition.value))continue; 114 | break; 115 | case '<=': 116 | //console.log(data[condition.key] + ' <= ' + condition.value); 117 | if(!(data[condition.key] <= condition.value))continue; 118 | break; 119 | case '>': 120 | //console.log(data[condition.key] + ' > ' + condition.value); 121 | if(!(data[condition.key] > condition.value))continue; 122 | break; 123 | case '>=': 124 | //console.log(data[condition.key] + ' >= ' + condition.value); 125 | if(!(data[condition.key] >= condition.value))continue; 126 | break; 127 | default: 128 | throw new Error('Unknown relation in tree: ', condition.relation, condition); 129 | } 130 | 131 | if(typeof condition.className !== 'undefined') 132 | return condition.className; 133 | 134 | conditions = condition.children; 135 | hasMatch = true; 136 | break; 137 | } 138 | 139 | if(!hasMatch){ 140 | console.log(conditions); 141 | throw new Error('No match in conditions', conditions); 142 | } 143 | }while(hasMatch); 144 | } 145 | 146 | this.classify = classify; 147 | parseTrees(); 148 | } -------------------------------------------------------------------------------- /webserver/public/js/vis/transitions/moveExisting.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash') 2 | , async = require('async') 3 | , smoothLine = require('../helpers/smoothLine.js') 4 | , d3 = require('d3') 5 | ; 6 | 7 | module.exports = function(options, elNew, elExisting, renderGraph, node, label, link){ 8 | function getScalingFactor(d){ 9 | if(!_.isUndefined(d.focused) && d.focused == true && !_.isUndefined(d.tfidf) && !_.isNaN(d.tfidf)) 10 | return d.tfidf; 11 | return d.pageRank; 12 | } 13 | 14 | /* 15 | Bereits vorhandene Nodes und Links zur neuen Position bewegen 16 | */ 17 | function moveExisting(done){ 18 | console.log(3); 19 | async.parallel([ 20 | function(doneTransition){ 21 | var n = 0; 22 | var transition = elExisting.labels 23 | .attr('transform', function(d){ 24 | if(!_.isUndefined(d.prevData)){ 25 | return 'translate(' + Math.round(d.prevData.x) + ',' + Math.round(d.prevData.y) + ') scale(' + (getScalingFactor(d.prevData) / 2 + 0.75) + ')'; 26 | }else{ 27 | return 'translate(' + Math.round(d.x) + ',' + Math.round(d.y) + ') scale(' + (getScalingFactor(d) / 2 + 0.75) + ')'; 28 | } 29 | }) 30 | .transition() 31 | .duration(options.transitionUpdate) 32 | .attrTween('transform', function(d,i,a){ 33 | var x = [d.x, d.x] 34 | , y = [d.y, d.y] 35 | , p = [getScalingFactor(d), getScalingFactor(d)] 36 | ; 37 | 38 | if(!_.isUndefined(d.prevData)){ 39 | x[0] = d.prevData.x; 40 | y[0] = d.prevData.y; 41 | p[0] = getScalingFactor(d.prevData); 42 | } 43 | 44 | return function(t){ 45 | var ret = 'translate(' + Math.round(x[0] * (1 - t) + x[1] * t) + ',' + Math.round(y[0] * (1 - t) + y[1] * t) + ') scale(' + (Math.round(p[0] * (1 - t) + p[1] * t) / 2 + 0.75) + ')'; 46 | return ret; 47 | } 48 | }).each('end', function(){ 49 | if(transition.size() == ++n){ 50 | console.log('transition ended'); 51 | doneTransition(); 52 | } 53 | }); 54 | 55 | if(transition.size() == 0) 56 | doneTransition(); 57 | }, 58 | function(doneTransition){ 59 | var n = 0; 60 | var transition = elExisting.links.selectAll('path') 61 | .attr('d', function(d){ 62 | var sx = d.source.x 63 | , sy = d.source.y 64 | , tx = d.target.x 65 | , ty = d.target.y 66 | ; 67 | 68 | if(!_.isUndefined(d.source.prevData)){ 69 | sx = d.source.prevData.x; 70 | sy = d.source.prevData.y; 71 | } 72 | 73 | if(!_.isUndefined(d.target.prevData)){ 74 | tx = d.target.prevData.x; 75 | ty = d.target.prevData.y; 76 | } 77 | 78 | return smoothLine({x: sx, y: sy}, {x:tx, y:ty}); 79 | }) 80 | .transition() 81 | .duration(options.transitionUpdate) 82 | .attrTween('d', function(d,i,a){ 83 | var sx = [d.source.x, d.source.x] 84 | , sy = [d.source.y, d.source.y] 85 | , tx = [d.target.x, d.target.x] 86 | , ty = [d.target.y, d.target.y] 87 | ; 88 | 89 | if(!_.isUndefined(d.source.prevData)){ 90 | sx[0] = d.source.prevData.x; 91 | sy[0] = d.source.prevData.y; 92 | } 93 | 94 | if(!_.isUndefined(d.target.prevData)){ 95 | tx[0] = d.target.prevData.x; 96 | ty[0] = d.target.prevData.y; 97 | } 98 | 99 | return function(t){ 100 | 101 | 102 | return smoothLine( 103 | { 104 | x: Math.round(sx[0] * (1 - t) + sx[1] * t), 105 | y: Math.round(sy[0] * (1 - t) + sy[1] * t) 106 | }, 107 | { 108 | x:Math.round(tx[0] * (1 - t) + tx[1] * t), 109 | y:Math.round(ty[0] * (1 - t) + ty[1] * t) 110 | }); 111 | } 112 | }).each('end', function(){ 113 | if(transition.size() == ++n){ 114 | console.log('link transition ended'); 115 | doneTransition(); 116 | } 117 | }); 118 | if(transition.size() == 0) 119 | doneTransition(); 120 | } 121 | ], function(){ 122 | console.log('Done transition'); 123 | setTimeout(done, 0); 124 | }); 125 | } 126 | 127 | this.moveExisting = moveExisting; 128 | } -------------------------------------------------------------------------------- /webserver/models/RelationSentence.js: -------------------------------------------------------------------------------- 1 | var DatasourceMysql = require('../datasources/mysql.js') 2 | , async = require('async') 3 | , _ = require('lodash') 4 | ; 5 | 6 | module.exports = function(db){ 7 | //console.log('RelationSentence'); 8 | var name = 'RelationSentence' 9 | , table = 'relations_sentences' 10 | , models = _.defaults({RelationSentence: this}, arguments[1] || {}) 11 | , datasource = new DatasourceMysql(db, name, table) 12 | , Sentence = models.Sentence || (new (require('./Sentence.js'))(db, models)) 13 | , Relationtype = models.Relationtype || (new (require('./Relationtype.js'))(db, models)) 14 | ; 15 | 16 | function countByRelation(relations, callback){ 17 | datasource.find('list', { 18 | fields: ['relation_id', 'count(*) `total`'], 19 | conditions: { 20 | relation_id: relations, 21 | is_deleted: 0 22 | }, 23 | group: 'relation_id' 24 | }, callback); 25 | } 26 | 27 | this.countByRelation = countByRelation; 28 | 29 | function sentencesWithRelation(/*relationId,[ options,]callback*/){ 30 | var relationId = arguments[0] 31 | , options = { 32 | withArticles: false, 33 | withTypes: false, 34 | highlight: null 35 | } 36 | , callback = arguments[arguments.length - 1] 37 | ; 38 | 39 | if(arguments.length > 2) 40 | options = _.defaults(arguments[1], options); 41 | 42 | datasource.find('all', { 43 | fields: ['sentence_id', 'relationtype_id', 'direction'], 44 | conditions: { 45 | relation_id: relationId, 46 | is_deleted: 0 47 | } 48 | }, (err, relations_sentences) => { 49 | console.log(relations_sentences); 50 | 51 | if(err)return setImmediate(() => callback(err)); 52 | if(_.isEmpty(relations_sentences))return setImmediate(() => callback(null, [])); 53 | 54 | var idMap = {}; 55 | relations_sentences.forEach((sentence, i) => idMap[sentence.sentence_id] = i); 56 | Sentence.findById(_.keys(idMap), {withArticles: options.withArticles, highlight: options.highlight}, (err, sentences) => { 57 | if(err)return setImmediate(() => callback(err)); 58 | 59 | sentences.forEach(sentence => { 60 | var key = idMap[sentence.id]; 61 | sentence.RelationSentence = relations_sentences[key]; 62 | }); 63 | 64 | if(!options.withTypes) 65 | return setImmediate(() => callback(null, sentences)); 66 | 67 | idMap = {}; 68 | sentences.forEach((sentence, i) => { 69 | if(!_.isNull(sentence.RelationSentence.relationtype_id)) 70 | (idMap[sentence.RelationSentence.relationtype_id] || (idMap[sentence.RelationSentence.relationtype_id] = [])).push(i); 71 | }); 72 | 73 | if(_.isEmpty(idMap)) 74 | return setImmediate(() => callback(null, sentences)); 75 | 76 | Relationtype.find(_.keys(idMap), (err, relationtypes) => { 77 | if(err)return setImmediate(() => callback(err)); 78 | if(!_.isEmpty(relationtypes)) 79 | _.forOwn(relationtypes, (relationtype, relationtype_id) => { 80 | idMap[relationtype_id].forEach((key) => sentences[key].Relationtype = relationtype); 81 | }); 82 | 83 | setImmediate(() => callback(null, sentences)); 84 | }); 85 | }); 86 | }); 87 | } 88 | 89 | this.sentencesWithRelation = sentencesWithRelation; 90 | 91 | function reassign(/*targetRelationId, sourceRelationId, changelogId, callback*/){ 92 | var targetRelationId = arguments[0] 93 | , sourceRelationId = arguments[1] 94 | , changelogId = arguments[2] 95 | , callback = arguments[arguments.length - 1] 96 | ; 97 | 98 | datasource.update(changelogId, { 99 | values: { 100 | relation_id: targetRelationId 101 | }, 102 | conditions: { 103 | relation_id: sourceRelationId 104 | } 105 | }, callback); 106 | } 107 | 108 | this.reassign = reassign; 109 | 110 | function add(changelogId, data, callback){ 111 | var values = { 112 | relation_id: data.relation_id, 113 | sentence_id: data.sentence_id, 114 | relationtype_id: data.relationtype_id || null, 115 | is_deleted: 0 116 | }; 117 | 118 | datasource.insert(changelogId, { 119 | values: values 120 | }, (err, insertId) => { 121 | if(err)return setImmediate(() => callback(err)); 122 | 123 | setImmediate(() => callback(null, insertId)); 124 | }); 125 | } 126 | 127 | this.add = add; 128 | } 129 | -------------------------------------------------------------------------------- /webserver/public/js/libs/pagerank.js: -------------------------------------------------------------------------------- 1 | //https://github.com/stevemacn/PageRank/blob/master/lib/pagerank.js 2 | 3 | // Initialize 4 | // ---------- 5 | function Pagerank(nodeMatrix, linkProb, tolerance, callback, debug) { 6 | //**OutgoingNodes:** represents an array of nodes. Each node in this 7 | //array contains an array of nodes to which the corresponding node has 8 | //outgoing links. 9 | this.outgoingNodes = nodeMatrix; 10 | //**LinkProb:** a value ?? 11 | this.linkProb = linkProb; 12 | //**Tolerance:** the point at which a solution is deemed optimal. 13 | //Higher values are more accurate, lower values are faster to computer. 14 | this.tolerance = tolerance; 15 | this.callback = callback; 16 | 17 | //Number of outgoing nodes 18 | this.pageCount = Object.keys(this.outgoingNodes).length; 19 | //**Coeff:** coefficient for the likelihood that a page will be visited. 20 | this.coeff = (1-linkProb)/this.pageCount; 21 | 22 | this.probabilityNodes = !(nodeMatrix instanceof Array) ? {} : []; 23 | this.incomingNodes = !(nodeMatrix instanceof Array) ? {} : []; 24 | this.debug=debug; 25 | 26 | this.startRanking = function () { 27 | 28 | //we initialize all of our probabilities 29 | var initialProbability = 1/this.pageCount, 30 | outgoingNodes = this.outgoingNodes, i, a, index; 31 | 32 | //rearray the graph and generate initial probability 33 | for (i in outgoingNodes) { 34 | this.probabilityNodes[i]=initialProbability; 35 | for (a in outgoingNodes[i]) { 36 | index = outgoingNodes[i][a]; 37 | if (!this.incomingNodes[index]) { 38 | this.incomingNodes[index]=[]; 39 | } 40 | this.incomingNodes[index].push(i); 41 | } 42 | } 43 | 44 | //if debug is set, print each iteration 45 | if (this.debug) this.reportDebug(1) 46 | 47 | this.iterate(1); 48 | }; 49 | 50 | this.reportDebug = function (count) { 51 | console.log("____ITERATION "+count+"____"); 52 | console.log("Pages: " + Object.keys(this.outgoingNodes).length); 53 | console.log("outgoing %j", this.outgoingNodes); 54 | console.log("incoming %j",this.incomingNodes); 55 | console.log("probability %j",this.probabilityNodes); 56 | }; 57 | 58 | this.iterate = function(count) { 59 | var result = []; 60 | var resultHash={}; 61 | var prob, ct, b, a, sum, res, max, min; 62 | 63 | //For each node, we look at the incoming edges and 64 | //the weight of the node connected via each edge. 65 | //This weight is divided by the total number of 66 | //outgoing edges from each weighted node and summed to 67 | //determine the new weight of the original node. 68 | for (b in this.probabilityNodes) { 69 | sum = 0; 70 | if( this.incomingNodes[b] ) { 71 | for ( a=0; a 2 | 3 | 4 | 5 | 6 | 7 | Storyfinder 8 | 21 | 22 | 23 |
24 | 28 |
29 |
30 | 40 | 48 |
49 |
    50 |
  • 51 | 52 |
  • 53 |
54 |
55 |
56 |
57 |
58 | menu 59 |
Global network
60 | searchclose 61 | 62 |
63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 |
75 |
76 | arrow_back 77 |
Global network
78 | Subtitle 79 |
    80 |
  • 81 | Sources 82 |
  • 83 |
  • 84 | Relations 85 |
  • 86 |
87 |
88 |
89 |
90 |
91 | 92 |
93 |
94 | 95 |
96 |
97 | 98 |
99 |
100 |
101 |
102 |
103 |
104 |

Add current website to Storyfinder?

105 | 106 | The current website may not contain any suitable content. Do you want to add it to Storyfinder anyway? 107 | 108 |
109 | CancelAdd 110 |
111 |
112 |
113 |
114 |
115 | 116 | 123 | 124 | 125 | 126 | -------------------------------------------------------------------------------- /webserver/libs/changelog.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash') 2 | , async = require('async') 3 | ; 4 | 5 | function buildConditions(conditions){ 6 | var args = []; 7 | 8 | for(var key in conditions){ 9 | if(key.match(/^\d+$/)){ 10 | args.push(conditions[key]); 11 | }else if(key == 'or'){ 12 | args.push(buildConditions(conditions[key]).join(' or ')); 13 | }else if(key == 'and'){ 14 | args.push(buildConditions(conditions[key]).join(' and ')); 15 | }else{ 16 | var rel = '='; 17 | var val = conditions[key]; 18 | if(key.indexOf(' ') != -1){ 19 | key = key.split(' '); 20 | rel = key[1]; 21 | key = key[0]; 22 | } 23 | 24 | if(conditions[key] == null && rel == '='){ 25 | args.push('`' + key + '` IS NULL'); 26 | }else{ 27 | //console.log(typeof val); 28 | 29 | if(_.isArray(val)){ 30 | val = '("' + _.map(val, function(v){ 31 | if(isNaN(v)) 32 | return v.replace(/\"/g,"\\\"") 33 | else 34 | return v; 35 | }).join('","') + '")'; 36 | 37 | if(rel == '<>' || rel == '!='){ 38 | rel = ' NOT IN '; 39 | }else{ 40 | rel = ' IN '; 41 | } 42 | }else if(isNaN(val)) 43 | val = '"' + val.replace(/\"/g,"\\\"") + '"'; 44 | 45 | args.push('`' + key + '` ' + rel + ' ' + val); 46 | } 47 | } 48 | } 49 | 50 | return args; 51 | } 52 | 53 | module.exports.update = function update(connection, userId){ 54 | var date = (new Date()).toISOString().slice(0,19).replace('T',' ') 55 | ; 56 | 57 | var options = _.defaults(arguments[2], { 58 | values: null, 59 | table: null, 60 | conditions: null, 61 | limit: null 62 | }); 63 | 64 | var callback = arguments[3] 65 | , fields = _.keys(options.values) 66 | , where = buildConditions(options.conditions).join(' and ') 67 | , limit = '' 68 | ; 69 | 70 | if(!_.isNull(options.limit)){ 71 | if(_.isArray(options.limit)){ 72 | options.limit = _.map(options.limit, function(l){ 73 | if(isNaN(l)) 74 | return 0; 75 | return l; 76 | }); 77 | limit = ' LIMIT ' + options.limit.join(','); 78 | }else if(!isNaN(options.limit)) 79 | limit = ' LIMIT ' + options.limit; 80 | } 81 | 82 | var q = 'SELECT `id`, `' + fields.join('`,`') + '` FROM `' + options.table + '` WHERE ' + where + limit; 83 | //console.log(q); 84 | connection.query(q, function(err, results, f){ 85 | if(err){ 86 | setImmediate(function(){callback(err);}); 87 | return; 88 | } 89 | 90 | if(!_.isNull(results)){ 91 | var updates = []; 92 | 93 | async.eachSeries(results, function(row, nextRow){ 94 | var changes = {}; 95 | for(var i = 0;i < fields.length; i++){ 96 | if(row[fields[i]] != options.values[fields[i]]){ 97 | changes[fields[i]] = row[fields[i]]; 98 | } 99 | } 100 | 101 | if(!_.isEmpty(changes)){ 102 | updates.push([row.id, JSON.stringify(changes), options.table]); 103 | } 104 | 105 | setImmediate(nextRow); 106 | }, function(err){ 107 | if(err){ 108 | callback(err); 109 | return; 110 | } 111 | 112 | q = 'INSERT INTO changelogs (`created`, `user_id`) VALUES (?, ?)'; 113 | //console.log(q); 114 | connection.query(q, [date, userId], function(err, result){ 115 | if(err){ 116 | callback(err); 117 | return; 118 | } 119 | 120 | var changelog_id = result.insertId; 121 | 122 | async.each(updates, function(update, nextUpdate){ 123 | //console.log('INSERT INTO changelogs_updates (`changelog_id`, `foreign_id`, `vals`, `tbl`) VALUES (' + changelog_id + ', ?, ?, ?)'); 124 | connection.query('INSERT INTO changelogs_updates (`changelog_id`, `foreign_id`, `vals`, `tbl`) VALUES (' + changelog_id + ', ?, ?, ?)', update, nextUpdate); 125 | }, function(err){ 126 | if(err){ 127 | callback(err); 128 | return; 129 | } 130 | 131 | //console.log('UPDATE `' + options.table + '` SET ' + _.keys(options.values).join('=?, ') + '=? WHERE ' + where + limit); 132 | 133 | connection.query('UPDATE `' + options.table + '` SET ' + _.keys(options.values).join('=?, ') + '=? WHERE ' + where + limit, _.values(options.values), function(err){ 134 | if(err){ 135 | callback(err); 136 | return; 137 | } 138 | 139 | 140 | setImmediate(function(){ 141 | callback(null, changelog_id); 142 | }); 143 | }); 144 | }); 145 | }); 146 | }); 147 | }else{ 148 | console.log('RESULT IS NULL!!!', q); 149 | } 150 | }); 151 | } -------------------------------------------------------------------------------- /webserver/controllers/ArticlesController.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash') 2 | , async = require('async') 3 | , fs = require('fs') 4 | , path = require('path') 5 | , ensureLoggedIn = require('connect-ensure-login').ensureLoggedIn 6 | , evallog = new (require('../libs/evallog.js'))() 7 | ; 8 | 9 | module.exports = function(connection, app, passport){ 10 | var User = new (require('../models/User.js'))(connection) 11 | , Site = new (require('../models/Site.js'))(connection) 12 | , Article = new (require('../models/Article.js'))(connection) 13 | , Collection = new (require('../models/Collection.js'))(connection) 14 | ; 15 | 16 | app.get('/Articles/:articleId', ensureLoggedIn((process.env.PATH_PREFIX || '/') + 'login'), function (req, res) { 17 | var userId = req.user.id 18 | , articleId = req.params.articleId 19 | ; 20 | 21 | evallog.log('Open article ' + articleId); 22 | 23 | async.waterfall([ 24 | (next) => setImmediate(() => next(null, {article_id: articleId})), 25 | /* 26 | Get the article 27 | */ 28 | (memo, next) => { 29 | Article.findById(memo.article_id, 30 | (err, article) => { 31 | if(err)return setImmediate(() => next(err)); 32 | memo.Article = article; 33 | setImmediate(() => next(null, memo)); 34 | } 35 | ); 36 | }, 37 | /* 38 | Get the site containing the article 39 | */ 40 | (memo, next) => { 41 | Site.findById(memo.Article.site_id, 42 | (err, site) => { 43 | if(err)return setImmediate(() => next(err)); 44 | memo.Site = site; 45 | setImmediate(() => next(null, memo)); 46 | } 47 | ); 48 | }, 49 | /* 50 | Verify that the site belongs to one of the user's collection 51 | */ 52 | (memo, next) => { 53 | Collection.findById(memo.Site.collection_id, 54 | (err, collection) => { 55 | if(err)return setImmediate(() => next(err)); 56 | 57 | if(collection.user_id != userId) 58 | memo = null; 59 | 60 | setImmediate(() => next(null, memo)); 61 | } 62 | ); 63 | } 64 | ], 65 | (err, result) => { 66 | if(err){ 67 | console.log(err); 68 | return setImmediate(() => res.sendStatus(500)); 69 | } 70 | 71 | if(_.isNull(result)) 72 | res.sendStatus(404); 73 | else 74 | res.render('Articles/get', result); 75 | }); 76 | }); 77 | 78 | app.put('/Articles/:articleId/image', passport.authenticate('basic', {session: false}), function(req, res){ 79 | var userId = req.user.id 80 | , articleId = req.params.articleId 81 | , img = req.body.image.replace(/^data:image\/png;base64,/, "") 82 | ; 83 | 84 | async.waterfall([ 85 | (next) => setImmediate(() => next(null, { 86 | user_id: userId 87 | })), 88 | _mGetCollection, //Get the id of user's default collection 89 | (memo, next) => { 90 | async.reduce(['..', 'public', 'images', '/' + memo.Collection.id, 'articles'], __dirname, (p, folder, nextFolder) => { 91 | p = path.join(p, folder); 92 | 93 | fs.stat(p, (err, stats) => { 94 | if(err && err.code == 'ENOENT') { 95 | fs.mkdir(p, (err) => { 96 | if(err)return setImmediate(() => nextFolder(err)); 97 | 98 | setImmediate(() => nextFolder(null, p)); 99 | }); 100 | }else if(err){ 101 | return setImmediate(() => nextFolder(err)); 102 | }else{ 103 | return setImmediate(() => nextFolder(null, p)); 104 | } 105 | }); 106 | }, (err, p) => { 107 | if(err)return setImmediate(() => next(err)); 108 | 109 | memo.path = p; 110 | setImmediate(() => next(null, memo)); 111 | }); 112 | }, 113 | (memo, next) => { 114 | fs.writeFile(path.join(memo.path, articleId + '.png'), img, 'base64', function(err){ 115 | if(err)return setImmediate(() => next(err)); 116 | 117 | setImmediate(() => next(null, memo)); 118 | }); 119 | } 120 | ], 121 | (err, result) => { 122 | if(err){ 123 | console.log(err); 124 | res.sendStatus(500); 125 | return false; 126 | } 127 | 128 | res.send({ 129 | success: true 130 | }); 131 | }); 132 | }); 133 | 134 | /* 135 | Memo functions 136 | */ 137 | /* 138 | Get the id of user's default collection 139 | */ 140 | function _mGetCollection(memo, next){ 141 | Collection.getDefault(memo.user_id, 142 | (err, collection) => { 143 | if(err)return setImmediate(() => next(err)); 144 | 145 | memo.Collection = collection; 146 | 147 | setImmediate(() => next(null, memo)) 148 | } 149 | ); 150 | } 151 | } -------------------------------------------------------------------------------- /webserver/public/js/search.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash') 2 | , async = require('async') 3 | , Delegate = require('dom-delegate') 4 | , tplResults = require('./templates/search/results.hbs') 5 | , actions = require('./actions/StoryfinderActions.js') 6 | ; 7 | 8 | module.exports = function(store, vis){ 9 | var inputDelegate = null 10 | , searchDelegate = null 11 | , searchQ = async.queue(search, 1) 12 | , elResults = document.getElementById('search-results') 13 | ; 14 | 15 | function initialize(){ 16 | inputDelegate = new Delegate(document.body.querySelector('#graph-title')); 17 | 18 | inputDelegate.on('keyup', '.btn-search > input', function(event){ 19 | var value = event.target.value; 20 | 21 | if(value.replace(/\s+/g, '').length == 0){ 22 | event.target.classList.remove('active'); 23 | event.target.parentNode.classList.remove('active'); 24 | elResults.classList.remove('active'); 25 | }else{ 26 | elResults.classList.add('active'); 27 | event.target.classList.add('active'); 28 | event.target.parentNode.classList.add('active'); 29 | searchQ.push(value); 30 | } 31 | }); 32 | 33 | inputDelegate.on('click', '.btn-search .icon-close', function(event){ 34 | clear(); 35 | return false; 36 | }); 37 | 38 | searchDelegate = new Delegate(elResults); 39 | 40 | searchDelegate.on('click', '.site .show-graph', function(event){ 41 | clear(); 42 | store.dispatch(actions.toLocalgraph(event.target.getAttribute('data-id'))); 43 | return false; 44 | }); 45 | 46 | searchDelegate.on('click', '.entity', function(event){ 47 | clear(); 48 | vis.highlight(parseInt(event.target.getAttribute('data-id')), () => { 49 | vis.selectNode('.label[data-id="' + parseInt(event.target.getAttribute('data-id')) + '"]'); 50 | }); 51 | return false; 52 | }); 53 | } 54 | 55 | function clear(){ 56 | var input = document.body.querySelector('#graph-title .btn-search > input'); 57 | if(input != null){ 58 | input.value = ''; 59 | input.blur(); 60 | input.classList.remove('active'); 61 | input.parentNode.classList.remove('active'); 62 | } 63 | if(elResults != null) 64 | elResults.classList.remove('active'); 65 | } 66 | 67 | function search(searchValue, callback){ 68 | if(searchQ.length() > 0) 69 | return setTimeout(callback, 0); //Process only the latest query 70 | 71 | fetch('/Entities/search?q=' + encodeURIComponent(searchValue), { 72 | credentials: 'same-origin' 73 | }) 74 | .then(response => response.json()) 75 | .then(json => { 76 | showResults(json); 77 | setTimeout(callback, 0); 78 | }).catch(err => { 79 | console.log(err); 80 | setTimeout(callback, 0); 81 | }); 82 | } 83 | 84 | function showResults(results){ 85 | results.Sites.forEach(function(site){ 86 | if(_.isUndefined(site.Article.Sentences)) 87 | site.Article.Sentences = []; 88 | 89 | site.Article.Sentences = _.map(site.Article.Sentences, (sentence) => { 90 | return sentence.text.split(results.search).join('' + results.search + ''); 91 | }); 92 | 93 | site.sentencesMore = (site.Article.Sentences.length > 3)?(site.Article.Sentences.length - 3):false; 94 | }); 95 | 96 | elResults.innerHTML = tplResults(results); 97 | 98 | //Scale elements equaly 99 | var prevY = null 100 | , sitesByY = {} 101 | ; 102 | 103 | function rescale(y){ 104 | var row = sitesByY[y]; 105 | 106 | var max = 0; 107 | row.forEach(function(c){ 108 | if(c.height > max) 109 | max = c.height; 110 | }); 111 | 112 | row.forEach(function(c){ 113 | var s = c.el.querySelector('.sentences'); 114 | if(s == null)return; 115 | var h = s.getBoundingClientRect().height; 116 | s.style.maxHeight = h + ((max - c.height)) + 'px'; 117 | s.style.minHeight = h + ((max - c.height)) + 'px'; 118 | }); 119 | } 120 | 121 | var sites = document.body.querySelectorAll('.site'); 122 | 123 | for(var site of sites){ 124 | var rect = site.getBoundingClientRect(); 125 | var y = rect.top; 126 | 127 | if(prevY != y && prevY != null){ 128 | rescale(prevY); 129 | 130 | rect = site.getBoundingClientRect(); 131 | y = rect.top; 132 | } 133 | 134 | if(_.isUndefined(sitesByY[y])) 135 | sitesByY[y] = []; 136 | 137 | sitesByY[y].push({ 138 | el: site, 139 | height: rect.height 140 | }); 141 | 142 | prevY = y; 143 | } 144 | 145 | if(!_.isNull(prevY)) 146 | rescale(prevY); 147 | } 148 | 149 | this.clear = clear; 150 | 151 | initialize(); 152 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | #### 2 | # created by steffen 3 | webserver/public/images/ 4 | webserver/**/*-data 5 | release 6 | 7 | plugin/dist/ 8 | 9 | #### 10 | # Created by https://www.gitignore.io/api/node,bower,eclipse,webstorm+all,intellij+all 11 | 12 | ### Bower ### 13 | bower_components 14 | .bower-cache 15 | .bower-registry 16 | .bower-tmp 17 | 18 | ### Eclipse ### 19 | 20 | .metadata 21 | bin/ 22 | tmp/ 23 | *.tmp 24 | *.bak 25 | *.swp 26 | *~.nib 27 | local.properties 28 | .settings/ 29 | .loadpath 30 | .recommenders 31 | 32 | # External tool builders 33 | .externalToolBuilders/ 34 | 35 | # Locally stored "Eclipse launch configurations" 36 | *.launch 37 | 38 | # PyDev specific (Python IDE for Eclipse) 39 | *.pydevproject 40 | 41 | # CDT-specific (C/C++ Development Tooling) 42 | .cproject 43 | 44 | # Java annotation processor (APT) 45 | .factorypath 46 | 47 | # PDT-specific (PHP Development Tools) 48 | .buildpath 49 | 50 | # sbteclipse plugin 51 | .target 52 | 53 | # Tern plugin 54 | .tern-project 55 | 56 | # TeXlipse plugin 57 | .texlipse 58 | 59 | # STS (Spring Tool Suite) 60 | .springBeans 61 | 62 | # Code Recommenders 63 | .recommenders/ 64 | 65 | # Scala IDE specific (Scala & Java development for Eclipse) 66 | .cache-main 67 | .scala_dependencies 68 | .worksheet 69 | 70 | ### Eclipse Patch ### 71 | # Eclipse Core 72 | .project 73 | 74 | # JDT-specific (Eclipse Java Development Tools) 75 | .classpath 76 | 77 | ### Intellij+all ### 78 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm 79 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 80 | 81 | # User-specific stuff: 82 | .idea/**/workspace.xml 83 | .idea/**/tasks.xml 84 | .idea/dictionaries 85 | 86 | # Sensitive or high-churn files: 87 | .idea/**/dataSources/ 88 | .idea/**/dataSources.ids 89 | .idea/**/dataSources.xml 90 | .idea/**/dataSources.local.xml 91 | .idea/**/sqlDataSources.xml 92 | .idea/**/dynamic.xml 93 | .idea/**/uiDesigner.xml 94 | 95 | # Gradle: 96 | .idea/**/gradle.xml 97 | .idea/**/libraries 98 | 99 | # CMake 100 | cmake-build-debug/ 101 | 102 | # Mongo Explorer plugin: 103 | .idea/**/mongoSettings.xml 104 | 105 | ## File-based project format: 106 | *.iws 107 | 108 | ## Plugin-specific files: 109 | 110 | # IntelliJ 111 | /out/ 112 | 113 | # mpeltonen/sbt-idea plugin 114 | .idea_modules/ 115 | 116 | # JIRA plugin 117 | atlassian-ide-plugin.xml 118 | 119 | # Cursive Clojure plugin 120 | .idea/replstate.xml 121 | 122 | # Crashlytics plugin (for Android Studio and IntelliJ) 123 | com_crashlytics_export_strings.xml 124 | crashlytics.properties 125 | crashlytics-build.properties 126 | fabric.properties 127 | 128 | ### Intellij+all Patch ### 129 | # Ignores the whole idea folder 130 | # See https://github.com/joeblau/gitignore.io/issues/186 and https://github.com/joeblau/gitignore.io/issues/360 131 | 132 | .idea/ 133 | 134 | ### Node ### 135 | # Logs 136 | logs 137 | *.log 138 | npm-debug.log* 139 | yarn-debug.log* 140 | yarn-error.log* 141 | 142 | # Runtime data 143 | pids 144 | *.pid 145 | *.seed 146 | *.pid.lock 147 | 148 | # Directory for instrumented libs generated by jscoverage/JSCover 149 | lib-cov 150 | 151 | # Coverage directory used by tools like istanbul 152 | coverage 153 | 154 | # nyc test coverage 155 | .nyc_output 156 | 157 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 158 | .grunt 159 | 160 | # Bower dependency directory (https://bower.io/) 161 | 162 | # node-waf configuration 163 | .lock-wscript 164 | 165 | # Compiled binary addons (http://nodejs.org/api/addons.html) 166 | build/Release 167 | 168 | # Dependency directories 169 | node_modules/ 170 | jspm_packages/ 171 | 172 | # Typescript v1 declaration files 173 | typings/ 174 | 175 | # Optional npm cache directory 176 | .npm 177 | 178 | # Optional eslint cache 179 | .eslintcache 180 | 181 | # Optional REPL history 182 | .node_repl_history 183 | 184 | # Output of 'npm pack' 185 | *.tgz 186 | 187 | # Yarn Integrity file 188 | .yarn-integrity 189 | 190 | # dotenv environment variables file 191 | .env 192 | 193 | 194 | ### WebStorm+all ### 195 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm 196 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 197 | 198 | # User-specific stuff: 199 | 200 | # Sensitive or high-churn files: 201 | 202 | # Gradle: 203 | 204 | # CMake 205 | 206 | # Mongo Explorer plugin: 207 | 208 | ## File-based project format: 209 | 210 | ## Plugin-specific files: 211 | 212 | # IntelliJ 213 | 214 | # mpeltonen/sbt-idea plugin 215 | 216 | # JIRA plugin 217 | 218 | # Cursive Clojure plugin 219 | 220 | # Crashlytics plugin (for Android Studio and IntelliJ) 221 | 222 | ### WebStorm+all Patch ### 223 | # Ignores the whole idea folder 224 | # See https://github.com/joeblau/gitignore.io/issues/186 and https://github.com/joeblau/gitignore.io/issues/360 225 | 226 | 227 | # End of https://www.gitignore.io/api/node,bower,eclipse,webstorm+all,intellij+all 228 | -------------------------------------------------------------------------------- /plugin/src/contentstyle.css: -------------------------------------------------------------------------------- 1 | .storyfinder-main .de-tu-darmstadt-lt-storyfinder-root{transition:color .3s ease}.storyfinder-main .de-tu-darmstadt-lt-storyfinder-root.storyfinder-highlighted{color:#AAAAAA}.storyfinder-main .de-tu-darmstadt-lt-storyfinder-root.storyfinder-highlighted sf-entity.type-ORG{text-shadow:none !important;color:#C2185B;display:inline-block}.storyfinder-main .de-tu-darmstadt-lt-storyfinder-root.storyfinder-highlighted sf-entity.type-ORG:hover{transform:scale(1.1)}.storyfinder-main .de-tu-darmstadt-lt-storyfinder-root.storyfinder-highlighted sf-entity.type-LOC{text-shadow:none !important;color:#388E3C;display:inline-block}.storyfinder-main .de-tu-darmstadt-lt-storyfinder-root.storyfinder-highlighted sf-entity.type-LOC:hover{transform:scale(1.1)}.storyfinder-main .de-tu-darmstadt-lt-storyfinder-root.storyfinder-highlighted sf-entity.type-PER{text-shadow:none !important;color:#0288D1;display:inline-block}.storyfinder-main .de-tu-darmstadt-lt-storyfinder-root.storyfinder-highlighted sf-entity.type-PER:hover{transform:scale(1.1)}.storyfinder-main .de-tu-darmstadt-lt-storyfinder-root.storyfinder-highlighted sf-entity.type-OTH{text-shadow:none !important;color:#512DA8;display:inline-block}.storyfinder-main .de-tu-darmstadt-lt-storyfinder-root.storyfinder-highlighted sf-entity.type-OTH:hover{transform:scale(1.1)}.storyfinder-main .de-tu-darmstadt-lt-storyfinder-root.storyfinder-highlighted sf-entity.type-KEY{text-shadow:none !important;color:#E64A19;display:inline-block}.storyfinder-main .de-tu-darmstadt-lt-storyfinder-root.storyfinder-highlighted sf-entity.type-KEY:hover{transform:scale(1.1)}.storyfinder-main .de-tu-darmstadt-lt-storyfinder-root .entity{cursor:context-menu;white-space:nowrap}.storyfinder-main .de-tu-darmstadt-lt-storyfinder-root .entity sf-marker{display:inline-block;font-size:70%;text-rendering:auto;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;transform:translate(0, 0);padding-left:5px}.storyfinder-main .de-tu-darmstadt-lt-storyfinder-root sf-entity{text-shadow:0 0 5px #FFFF00}.storyfinder-main .de-tu-darmstadt-lt-storyfinder-root sf-entity.type-ORG{text-shadow:0 0 5px #E91E63;transition:all .3s ease}.storyfinder-main .de-tu-darmstadt-lt-storyfinder-root sf-entity.type-ORG.storyfinder-highlighted{text-shadow:0 0 2px #C2185B;color:#F8BBD0}.storyfinder-main .de-tu-darmstadt-lt-storyfinder-root sf-entity.type-LOC{text-shadow:0 0 5px #4CAF50;transition:all .3s ease}.storyfinder-main .de-tu-darmstadt-lt-storyfinder-root sf-entity.type-LOC.storyfinder-highlighted{text-shadow:0 0 2px #388E3C;color:#C8E6C9}.storyfinder-main .de-tu-darmstadt-lt-storyfinder-root sf-entity.type-PER{text-shadow:0 0 5px #03A9F4;transition:all .3s ease}.storyfinder-main .de-tu-darmstadt-lt-storyfinder-root sf-entity.type-PER.storyfinder-highlighted{text-shadow:0 0 2px #0288D1;color:#B3E5FC}.storyfinder-main .de-tu-darmstadt-lt-storyfinder-root sf-entity.type-OTH{text-shadow:0 0 5px #673AB7;transition:all .3s ease}.storyfinder-main .de-tu-darmstadt-lt-storyfinder-root sf-entity.type-OTH.storyfinder-highlighted{text-shadow:0 0 2px #512DA8;color:#D1C4E9}.storyfinder-main .de-tu-darmstadt-lt-storyfinder-root sf-entity.type-KEY{text-shadow:0 0 5px #FF5722;transition:all .3s ease}.storyfinder-main .de-tu-darmstadt-lt-storyfinder-root sf-entity.type-KEY.storyfinder-highlighted{text-shadow:0 0 2px #E64A19;color:#FFCCBC}.storyfinder-main .de-tu-darmstadt-lt-storyfinder-root.storyfinder{position:absolute;right:0;width:40px;height:auto;top:0;bottom:0;background-color:#FFFFFF;box-shadow:-2px 2px 2px rgba(0,0,0,0.3);z-index:99900;display:block}.storyfinder-main .de-tu-darmstadt-lt-storyfinder-root.storyfinder.fixed{top:0 !important;position:fixed}.storyfinder-main .de-tu-darmstadt-lt-storyfinder-root.storyfinder sf-toolbar .loading{width:40px;height:40px;font-size:30px;vertical-align:middle;line-height:40px;text-align:center}.storyfinder-main .de-tu-darmstadt-lt-storyfinder-root.storyfinder sf-toolbar .btn{height:40px;border-left:none;border-right:none;line-height:40px;vertical-align:middle}.storyfinder-main .de-tu-darmstadt-lt-storyfinder-root.storyfinder .articlemap{position:fixed;right:40px;bottom:0;box-shadow:-2px 2px 2px rgba(0,0,0,0.3)}.storyfinder-main .de-tu-darmstadt-lt-storyfinder-root.storyfinder .articlemap img{max-width:150px;height:350px}.storyfinder-main .storyfinder-overlay{position:absolute;top:0;left:0;pointer-events:none;z-index:999999}.storyfinder-main .storyfinder-overlay node{pointer-events:visiblePainted}.storyfinder-main .storyfinder-overlay path{stroke:#999999;fill:none}.storyfinder-main sf-source{position:absolute;right:130px;background-color:#FFFFFF;border:1px solid #CCCCCC;padding:3px;z-index:999998;max-width:120px;font-size:80%}.storyfinder-main sf-source img{width:120px}.storyfinder-main sf-marker-container{z-index:999999}.storyfinder-main sf-marker-container sf-marker{display:none;opacity:0;transition:opacity .1s}.storyfinder-main sf-marker-container sf-marker .marker-above,.storyfinder-main sf-marker-container sf-marker .marker-below{display:none}.storyfinder-main sf-marker-container sf-marker.above,.storyfinder-main sf-marker-container sf-marker.below{position:fixed;display:inline-block;opacity:1;padding:10px;background-color:rgba(0,0,0,0.8);color:#FFFFFF;transition:opacity .1s}.storyfinder-main sf-marker-container sf-marker.above{bottom:auto;top:0}.storyfinder-main sf-marker-container sf-marker.above .marker-above{display:inline-block}.storyfinder-main sf-marker-container sf-marker.below{top:auto;bottom:0}.storyfinder-main sf-marker-container sf-marker.below .marker-below{display:inline-block} -------------------------------------------------------------------------------- /webserver/public/js/register/register.js: -------------------------------------------------------------------------------- 1 | var Delegate = require('dom-delegate') 2 | ; 3 | 4 | require('es6-promise').polyfill(); 5 | require('isomorphic-fetch'); 6 | 7 | 8 | function disableForm(){ 9 | var formElements = document.querySelectorAll('form input, form button'); 10 | 11 | for(var element of formElements) 12 | element.setAttribute('disabled', 'disabled'); 13 | } 14 | 15 | function showLoading(){ 16 | document.querySelector('.avatar').classList.add('loading'); 17 | } 18 | 19 | function hideLoading(){ 20 | document.querySelector('.avatar').classList.remove('loading'); 21 | } 22 | 23 | function enableForm(){ 24 | var formElements = document.querySelectorAll('form input, form button'); 25 | 26 | for(var element of formElements) 27 | element.removeAttribute('disabled'); 28 | } 29 | 30 | function setError(element, msg){ 31 | var el = document.querySelector(element); 32 | el.classList.add('has-error'); 33 | 34 | if(typeof msg != 'undefined' && msg != null && msg.length > 0){ 35 | el.querySelector('.help-block').innerHTML = msg; 36 | } 37 | } 38 | 39 | function resetValidation(){ 40 | var formGroups = document.querySelectorAll('#frm-register .form-group'); 41 | 42 | for(var element of formGroups){ 43 | element.classList.remove('has-error'); 44 | element.querySelector('.help-block').innerHTML = ''; 45 | } 46 | } 47 | 48 | function register(){ 49 | resetValidation(); 50 | disableForm(); 51 | showLoading(); 52 | 53 | var formElements = document.querySelectorAll('#frm-register input, #frm-register button'); 54 | 55 | var data = {}; 56 | 57 | for(var element of formElements) 58 | data[element.getAttribute('name')] = element.value; 59 | 60 | if(data['password'] != data['password-verify']){ 61 | setError('.form-group-verify', 'Passwords don\'t match!'); 62 | setError('.form-group-password'); 63 | enableForm(); 64 | hideLoading(); 65 | return; 66 | } 67 | 68 | if(!data['username'].match(/^[-a-z0-9~!$%^&*_=+}{\'?]+(\.[-a-z0-9~!$%^&*_=+}{\'?]+)*@([a-z0-9_][-a-z0-9_]*(\.[-a-z0-9_]+)*\.(aero|arpa|biz|com|coop|edu|gov|info|int|mil|museum|name|net|org|pro|travel|mobi|[a-z][a-z])|([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}))(:[0-9]{1,5})?$/i)){ 69 | setError('.form-group-username', 'Please enter a valid Email address!'); 70 | enableForm(); 71 | hideLoading(); 72 | return; 73 | } 74 | 75 | if(data['password'].length < 8){ 76 | setError('.form-group-password', 'Password is too short! Please select a password with at least 8 characters.'); 77 | setError('.form-group-verify'); 78 | enableForm(); 79 | hideLoading(); 80 | return; 81 | } 82 | 83 | fetch('/Users', { 84 | method: 'PUT', 85 | headers: { 86 | "Content-Type": "application/json" 87 | }, 88 | body: JSON.stringify(data) 89 | }) 90 | .then(function(response) { 91 | if (response.status >= 400) { 92 | console.log(response.status); 93 | setError('.form-group-username', 'Error connection to the storyfinder server! Please try again later.'); 94 | enableForm(); 95 | hideLoading(); 96 | return null; 97 | } 98 | 99 | return response.json(); 100 | }) 101 | .then(function(json) { 102 | if(!json)return; 103 | 104 | if(!json.success){ 105 | setError('.form-group-username', json.message); 106 | enableForm(); 107 | hideLoading(); 108 | return null; 109 | } 110 | 111 | if(typeof parent != null) 112 | parent.postMessage(["msg", { 113 | action: 'userRegistered', 114 | username: data['username'], 115 | password: data['password'] 116 | }], "*"); 117 | 118 | document.location = '/'; 119 | }); 120 | } 121 | 122 | function login(){ 123 | resetValidation(); 124 | disableForm(); 125 | showLoading(); 126 | 127 | var formElements = document.querySelectorAll('form input'); 128 | 129 | var data = {}; 130 | var formData = []; 131 | 132 | for(var element of formElements){ 133 | formData.push(element.getAttribute('name') + '=' + encodeURIComponent(element.value)); 134 | data[element.getAttribute('name')] = element.value; 135 | } 136 | 137 | fetch('/login', { 138 | method: 'POST', 139 | credentials: 'same-origin', 140 | headers: { 141 | "Content-type": "application/x-www-form-urlencoded; charset=UTF-8" 142 | }, 143 | body: formData.join('&') 144 | }) 145 | .then(function(response) { 146 | if (response.status >= 400) { 147 | console.log(response.status); 148 | setError('.form-group-username', 'Login failed!'); 149 | enableForm(); 150 | hideLoading(); 151 | return null; 152 | } 153 | 154 | return response.json(); 155 | }) 156 | .then(function(json) { 157 | if(!json)return; 158 | 159 | if(!json.success){ 160 | setError('.form-group-username', json.message); 161 | enableForm(); 162 | hideLoading(); 163 | return null; 164 | } 165 | 166 | if(typeof parent != null) 167 | parent.postMessage(["msg", { 168 | action: 'userRegistered', 169 | username: data['username'], 170 | password: data['password'] 171 | }], "*"); 172 | 173 | document.location = '/'; 174 | }); 175 | } 176 | 177 | var frmRegister = document.querySelector('#frm-register'); 178 | if(frmRegister) 179 | frmRegister.addEventListener('submit', function(e){ 180 | e.preventDefault(); 181 | e.stopPropagation(); 182 | 183 | register(); 184 | }); 185 | 186 | var frmLogin = document.querySelector('#frm-login'); 187 | if(frmLogin) 188 | frmLogin.addEventListener('submit', function(e){ 189 | e.preventDefault(); 190 | e.stopPropagation(); 191 | 192 | login(); 193 | }); -------------------------------------------------------------------------------- /webserver/data/stopwords/german.txt: -------------------------------------------------------------------------------- 1 | 2 | | A German stop word list. Comments begin with vertical bar. Each stop 3 | | word is at the start of a line. 4 | 5 | | The number of forms in this list is reduced significantly by passing it 6 | | through the German stemmer. 7 | 8 | 9 | aber | but 10 | 11 | alle | all 12 | allem 13 | allen 14 | aller 15 | alles 16 | 17 | als | than, as 18 | also | so 19 | am | an + dem 20 | an | at 21 | 22 | ander | other 23 | andere 24 | anderem 25 | anderen 26 | anderer 27 | anderes 28 | anderm 29 | andern 30 | anderr 31 | anders 32 | 33 | auch | also 34 | auf | on 35 | aus | out of 36 | bei | by 37 | bin | am 38 | bis | until 39 | bist | art 40 | da | there 41 | damit | with it 42 | dann | then 43 | 44 | der | the 45 | den 46 | des 47 | dem 48 | die 49 | das 50 | 51 | daß | that 52 | 53 | derselbe | the same 54 | derselben 55 | denselben 56 | desselben 57 | demselben 58 | dieselbe 59 | dieselben 60 | dasselbe 61 | 62 | dazu | to that 63 | 64 | dein | thy 65 | deine 66 | deinem 67 | deinen 68 | deiner 69 | deines 70 | 71 | denn | because 72 | 73 | derer | of those 74 | dessen | of him 75 | 76 | dich | thee 77 | dir | to thee 78 | du | thou 79 | 80 | dies | this 81 | diese 82 | diesem 83 | diesen 84 | dieser 85 | dieses 86 | 87 | 88 | doch | (several meanings) 89 | dort | (over) there 90 | 91 | 92 | durch | through 93 | 94 | ein | a 95 | eine 96 | einem 97 | einen 98 | einer 99 | eines 100 | 101 | einig | some 102 | einige 103 | einigem 104 | einigen 105 | einiger 106 | einiges 107 | 108 | einmal | once 109 | 110 | er | he 111 | ihn | him 112 | ihm | to him 113 | 114 | es | it 115 | etwas | something 116 | 117 | euer | your 118 | eure 119 | eurem 120 | euren 121 | eurer 122 | eures 123 | 124 | für | for 125 | gegen | towards 126 | gewesen | p.p. of sein 127 | hab | have 128 | habe | have 129 | haben | have 130 | hat | has 131 | hatte | had 132 | hatten | had 133 | hier | here 134 | hin | there 135 | hinter | behind 136 | 137 | ich | I 138 | mich | me 139 | mir | to me 140 | 141 | 142 | ihr | you, to her 143 | ihre 144 | ihrem 145 | ihren 146 | ihrer 147 | ihres 148 | euch | to you 149 | 150 | im | in + dem 151 | in | in 152 | indem | while 153 | ins | in + das 154 | ist | is 155 | 156 | jede | each, every 157 | jedem 158 | jeden 159 | jeder 160 | jedes 161 | 162 | jene | that 163 | jenem 164 | jenen 165 | jener 166 | jenes 167 | 168 | jetzt | now 169 | kann | can 170 | 171 | kein | no 172 | keine 173 | keinem 174 | keinen 175 | keiner 176 | keines 177 | 178 | können | can 179 | könnte | could 180 | machen | do 181 | man | one 182 | 183 | manche | some, many a 184 | manchem 185 | manchen 186 | mancher 187 | manches 188 | 189 | mein | my 190 | meine 191 | meinem 192 | meinen 193 | meiner 194 | meines 195 | 196 | mit | with 197 | muss | must 198 | musste | had to 199 | nach | to(wards) 200 | nicht | not 201 | nichts | nothing 202 | noch | still, yet 203 | nun | now 204 | nur | only 205 | ob | whether 206 | oder | or 207 | ohne | without 208 | sehr | very 209 | 210 | sein | his 211 | seine 212 | seinem 213 | seinen 214 | seiner 215 | seines 216 | 217 | selbst | self 218 | sich | herself 219 | 220 | sie | they, she 221 | ihnen | to them 222 | 223 | sind | are 224 | so | so 225 | 226 | solche | such 227 | solchem 228 | solchen 229 | solcher 230 | solches 231 | 232 | soll | shall 233 | sollte | should 234 | sondern | but 235 | sonst | else 236 | über | over 237 | um | about, around 238 | und | and 239 | 240 | uns | us 241 | unse 242 | unsem 243 | unsen 244 | unser 245 | unses 246 | 247 | unter | under 248 | viel | much 249 | vom | von + dem 250 | von | from 251 | vor | before 252 | während | while 253 | war | was 254 | waren | were 255 | warst | wast 256 | was | what 257 | weg | away, off 258 | weil | because 259 | weiter | further 260 | 261 | welche | which 262 | welchem 263 | welchen 264 | welcher 265 | welches 266 | 267 | wenn | when 268 | werde | will 269 | werden | will 270 | wie | how 271 | wieder | again 272 | will | want 273 | wir | we 274 | wird | will 275 | wirst | willst 276 | wo | where 277 | wollen | want 278 | wollte | wanted 279 | würde | would 280 | würden | would 281 | zu | to 282 | zum | zu + dem 283 | zur | zu + der 284 | zwar | indeed 285 | zwischen | between -------------------------------------------------------------------------------- /webserver/controllers/GraphsController.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash') 2 | , async = require('async') 3 | , fs = require('fs') 4 | , ensureLoggedIn = require('connect-ensure-login').ensureLoggedIn 5 | , evallog = new (require('../libs/evallog.js'))() 6 | ; 7 | 8 | module.exports = function(connection, app, passport){ 9 | var User = new (require('../models/User.js'))(connection) 10 | , Site = new (require('../models/Site.js'))(connection) 11 | , Article = new (require('../models/Article.js'))(connection) 12 | , Collection = new (require('../models/Collection.js'))(connection) 13 | , Entity = new (require('../models/Entity.js'))(connection) 14 | , Relation = new (require('../models/Relation.js'))(connection) 15 | ; 16 | 17 | app.get('/Graphs', ensureLoggedIn((process.env.PATH_PREFIX || '/') + 'login'), function (req, res) { 18 | var userId = req.user.id 19 | 20 | async.waterfall([ 21 | (next) => { 22 | setImmediate(() => next(null, {user_id: userId})) 23 | }, 24 | _mGetCollection, //Get the id of user's default collection 25 | _mGetEntities, //Get the entities in the collection 26 | _mGetRelations //Get the relations of the entities 27 | ], 28 | (err, result) => { 29 | if(err){ 30 | console.log(err); 31 | return setImmediate(() => res.sendStatus(500)); 32 | } 33 | 34 | res.send(result); 35 | }); 36 | }); 37 | 38 | app.get('/Graphs/Site/:siteId', ensureLoggedIn((process.env.PATH_PREFIX || '/') + 'login'), function (req, res) { 39 | var userId = req.user.id 40 | , siteId = parseInt(req.params.siteId) 41 | ; 42 | 43 | evallog.log('Open graph ' + req.params.siteId); 44 | 45 | async.waterfall([ 46 | (next) => { 47 | setImmediate(() => next(null, {user_id: userId, site_id: siteId})) 48 | }, 49 | _mGetCollection, //Get the id of user's default collection 50 | _mGetSites, //Get Site(s) 51 | _mGetArticles, //Get newest Article 52 | _mGetEntities, //Get all entities for the site(s) 53 | _mGetRelations //Get the relations of the entities 54 | ], 55 | (err, result) => { 56 | if(err){ 57 | console.log(err); 58 | return setImmediate(() => res.sendStatus(500)); 59 | } 60 | 61 | res.send(result); 62 | }); 63 | }); 64 | 65 | app.get('/Graphs/Group/:siteIds', ensureLoggedIn((process.env.PATH_PREFIX || '/') + 'login'), function (req, res) { 66 | var userId = req.user.id 67 | , siteIds = _.map(req.params.siteIds.split(';'), siteId => parseInt(siteId)) 68 | ; 69 | 70 | evallog.log('Open group ' + siteIds.join(';')); 71 | 72 | async.waterfall([ 73 | (next) => { 74 | setImmediate(() => next(null, {user_id: userId, site_ids: siteIds})) 75 | }, 76 | _mGetCollection, //Get the id of user's default collection 77 | _mGetSites, //Get Site(s) 78 | _mGetArticles, //Get newest Article 79 | _mGetEntities, //Get all entities for the site(s) 80 | _mGetRelations //Get the relations of the entities 81 | ], 82 | (err, result) => { 83 | if(err){ 84 | console.log(err); 85 | return setImmediate(() => res.sendStatus(500)); 86 | } 87 | 88 | res.send(result); 89 | }); 90 | }); 91 | 92 | /* 93 | Memo functions: 94 | */ 95 | 96 | /* 97 | Get the id of user's default collection 98 | */ 99 | function _mGetCollection(memo, next){ 100 | Collection.getDefault(memo.user_id, 101 | (err, collection) => { 102 | if(err)return setImmediate(() => next(err)); 103 | 104 | memo.Collection = collection; 105 | 106 | setImmediate(() => next(null, memo)) 107 | } 108 | ); 109 | } 110 | 111 | /* 112 | Get the entities in the collection 113 | */ 114 | function _mGetEntities(memo, next){ 115 | var method = 'getAll' 116 | , param = memo.Collection.id 117 | ; 118 | 119 | if(!_.isEmpty(memo.Articles)){ 120 | method = 'getInArticle'; 121 | param = _.map(memo.Articles, 'id'); 122 | }else if(!_.isEmpty(memo.Article)){ 123 | method = 'getInArticle'; 124 | param = memo.Article.id; 125 | } 126 | 127 | Entity[method](param, 128 | (err, entities) => { 129 | if(err)return setImmediate(() => next(err)); 130 | 131 | memo.Entities = entities; 132 | 133 | setImmediate(() => next(null, memo)); 134 | } 135 | ); 136 | } 137 | 138 | /* 139 | Get the relations of the entities 140 | */ 141 | function _mGetRelations(memo, next){ 142 | Relation.getByEntities(_.map(memo.Entities, 'id'), 143 | {withTypes: true}, 144 | (err, relations) => { 145 | if(err)return setImmediate(() => next(err)); 146 | 147 | memo.Relations = relations; 148 | 149 | setImmediate(() => next(null, memo)); 150 | } 151 | ); 152 | } 153 | 154 | /* 155 | Get Site(s) 156 | */ 157 | function _mGetSites(memo, next){ 158 | var multi = false; 159 | if(!_.isUndefined(memo.site_ids)) 160 | multi = true; 161 | 162 | Site.getById(memo[multi?'site_ids':'site_id'], memo.Collection.id, 163 | (err, site) => { 164 | if(err)return setImmediate(() => next(err)); 165 | 166 | memo[multi?'Sites':'Site'] = site; 167 | 168 | setImmediate(() => next(null, memo)); 169 | } 170 | ); 171 | } 172 | 173 | /* 174 | Get newest articles of the sites 175 | */ 176 | function _mGetArticles(memo, next){ 177 | if(_.isEmpty(memo.Site) && _.isEmpty(memo.Sites))return setImmediate(() => next(null, memo)); 178 | 179 | var multi = false; 180 | if(!_.isUndefined(memo.Sites)) 181 | multi = true; 182 | 183 | Article.getLatest(multi?_.map(memo.Sites, 'id'):memo.Site.id, 184 | (err, article) => { 185 | if(err)return setImmediate(() => next(err)); 186 | 187 | memo[multi?'Articles':'Article'] = article; 188 | 189 | setImmediate(() => next(null, memo)); 190 | } 191 | ); 192 | } 193 | } -------------------------------------------------------------------------------- /webserver/controllers/RelationsController.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash') 2 | , async = require('async') 3 | , fs = require('fs') 4 | , ensureLoggedIn = require('connect-ensure-login').ensureLoggedIn 5 | , evallog = new (require('../libs/evallog.js'))() 6 | ; 7 | 8 | module.exports = function(connection, app, passport){ 9 | var User = new (require('../models/User.js'))(connection) 10 | , Relation = new (require('../models/Relation.js'))(connection) 11 | , Collection = new (require('../models/Collection.js'))(connection) 12 | , Entity = new (require('../models/Entity.js'))(connection) 13 | , RelationSentence = new (require('../models/RelationSentence.js'))(connection) 14 | ; 15 | 16 | app.get('/Relations/Entity/:entityId', ensureLoggedIn((process.env.PATH_PREFIX || '/') + 'login'), function (req, res) { 17 | var entityId = parseInt(req.params.entityId) 18 | , userId = req.user.id 19 | ; 20 | 21 | evallog.log('Get relations of entity ' + entityId); 22 | 23 | async.waterfall([ 24 | (next) => {setImmediate(() => next(null, {user_id: userId, entity_id: entityId}))}, 25 | _mGetCollection, //Get the id of user's default collection 26 | _mGetEntity, //Get the entity 27 | _mGetRelationsOfEntity, //Get relations of the entity 28 | ], 29 | (err, result) => { 30 | if(err){ 31 | console.log(err); 32 | return setImmediate(() => res.sendStatus(500)); 33 | } 34 | 35 | res.send(result); 36 | }); 37 | }); 38 | 39 | app.get('/Relations/:entity1Id/:entity2Id', ensureLoggedIn((process.env.PATH_PREFIX || '/') + 'login'), function (req, res) { 40 | var entity1Id = parseInt(req.params.entity1Id) 41 | , entity2Id = parseInt(req.params.entity2Id) 42 | , userId = req.user.id 43 | ; 44 | 45 | 46 | evallog.log('Get relations of entities ' + entity1Id + ' / ' + entity2Id); 47 | 48 | async.waterfall([ 49 | (next) => {setImmediate(() => next(null, {user_id: userId, entity_ids: [entity1Id, entity2Id]}))}, 50 | _mGetCollection, //Get the id of user's default collection 51 | _mGetEntity, //Get the entities 52 | _mGetRelationBetweenEntities, //Get relation 53 | _mGetSentences //Get sentences containing the relation 54 | ], 55 | (err, result) => { 56 | if(err){ 57 | console.log(err); 58 | return setImmediate(() => res.sendStatus(500)); 59 | } 60 | 61 | res.send(result); 62 | }); 63 | }); 64 | 65 | app.put('/Relations/:entity1Id/:entity2Id', ensureLoggedIn((process.env.PATH_PREFIX || '/') + 'login'), function (req, res) { 66 | var entity1Id = parseInt(req.params.entity1Id) 67 | , entity2Id = parseInt(req.params.entity2Id) 68 | , data = req.body 69 | , userId = req.user.id 70 | , sentenceId = req.body.sentence_id 71 | , label = req.body.label 72 | ; 73 | 74 | evallog.log('Set relation of entities ' + entity1Id + ' / ' + entity2Id + ' with label ' + label); 75 | 76 | async.waterfall([ 77 | (next) => { 78 | setImmediate(() => next(null, { 79 | user_id: userId, 80 | entity1_id: entity1Id, 81 | entity2_id: entity2Id, 82 | label: label, 83 | sentenceId: sentenceId 84 | })); 85 | }, 86 | _mGetCollection, 87 | Relation.create 88 | ], 89 | (err, result) => { 90 | if(err){ 91 | console.log(err); 92 | return setImmediate(() => res.sendStatus(500)); 93 | } 94 | 95 | result.Relation.Relationtype = result.Relationtype; 96 | 97 | res.send(result); 98 | }); 99 | }); 100 | 101 | /* 102 | Memo functions 103 | */ 104 | /* 105 | Get the id of user's default collection 106 | */ 107 | function _mGetCollection(memo, next){ 108 | Collection.getDefault(memo.user_id, 109 | (err, collection) => { 110 | if(err)return setImmediate(() => next(err)); 111 | 112 | memo.Collection = collection; 113 | 114 | setImmediate(() => next(null, memo)) 115 | } 116 | ); 117 | } 118 | 119 | /* 120 | Get the entity 121 | */ 122 | function _mGetEntity(memo, next){ 123 | Entity.getById(memo.entity_ids || memo.entity_id, { 124 | collection: memo.Collection.id, 125 | withMerged: true 126 | }, 127 | (err, entity) => { 128 | if(err)return setImmediate(() => next(err)); 129 | 130 | if(!_.isUndefined(memo.entity_ids)) 131 | memo.Entities = entity; 132 | else 133 | memo.Entity = entity; 134 | 135 | setImmediate(() => next(null, memo)); 136 | } 137 | ); 138 | } 139 | 140 | /* 141 | Get relations of the entity with neighbours 142 | */ 143 | function _mGetRelationsOfEntity(memo, next){ 144 | Relation.getByEntities(memo.Entity.id, { 145 | withCounts: true, 146 | withTypes: true, 147 | withEntities: true 148 | }, 149 | (err, relations) => { 150 | if(err)return setImmediate(() => next(err)); 151 | 152 | memo.Relations = relations; 153 | 154 | setImmediate(() => next(null, memo)); 155 | } 156 | ) 157 | } 158 | 159 | /* 160 | Get the relation between two entities 161 | */ 162 | function _mGetRelationBetweenEntities(memo, next){ 163 | Relation.getBetweenEntities(memo.entity_ids[0], memo.entity_ids[1], 164 | { 165 | withTypes: true 166 | }, 167 | (err, relation) => { 168 | if(err)return setImmediate(() => next(err)); 169 | 170 | memo.Relation = relation; 171 | 172 | setImmediate(() => next(null, memo)); 173 | } 174 | ) 175 | } 176 | 177 | /* 178 | Get sentences containing the relation 179 | */ 180 | function _mGetSentences(memo, next){ 181 | if(_.isEmpty(memo.Relation))return setImmediate(() => next(null, memo)); 182 | 183 | RelationSentence.sentencesWithRelation(memo.Relation.id, { 184 | withArticles: true, 185 | withTypes: true, 186 | highlight: memo.Entities 187 | }, (err, sentences) => { 188 | if(err)return setImmediate(() => next(err)); 189 | 190 | memo.Sentences = sentences; 191 | setImmediate(() => next(null, memo)); 192 | }); 193 | } 194 | } -------------------------------------------------------------------------------- /plugin/src/js-contentstyle/less/storyfinder.less: -------------------------------------------------------------------------------- 1 | @colors: #F44366, #E91E63, #9C27B0, #673AB7, #3F51B5, #2196F3, #03A9F4, #00BCD4, #009688, #4CAF50, #8BC34A, #FFC107, #FF9800, #FF5722, #795548; 2 | @colors2: #D32F2F, #C2185B, #7B1FA2, #512DA8, #303F9F, #1976D2, #0288D1, #0097A7, #00796B, #388E3C, #689F38, #FFA000, #F57C00, #E64A19, #5D4037; 3 | @colors3: #FFCDD2, #F8BBD0, #E1BEE7, #D1C4E9, #C5CAE9, #BBDEFB, #B3E5FC, #B2EBF2, #B2DFDB, #C8E6C9, #DCEDC8, #FFECB3, #FFE0B2, #FFCCBC, #D7CCC8; 4 | 5 | @ORG: 2; 6 | @OTH: 4; 7 | @PER: 7; 8 | @LOC: 10; 9 | @KEY: 14; 10 | 11 | .storyfinder-main { 12 | .typed-entity(@type, @idx){ 13 | @n: ~'.type-@{type}'; 14 | 15 | &@{n} { 16 | text-shadow: 0px 0px 5px extract(@colors, @idx); 17 | 18 | transition: all 0.3s ease; 19 | 20 | &.storyfinder-highlighted { 21 | text-shadow: 0px 0px 2px extract(@colors2, @idx); 22 | color: extract(@colors3, @idx); 23 | } 24 | } 25 | } 26 | 27 | .typed-entity-highlighted(@type, @idx){ 28 | @n: ~'.type-@{type}'; 29 | 30 | &@{n} { 31 | text-shadow: none !important; 32 | color: extract(@colors2, @idx); 33 | display: inline-block; 34 | 35 | &:hover { 36 | transform: scale(1.1); 37 | } 38 | } 39 | } 40 | 41 | .de-tu-darmstadt-lt-storyfinder-root { 42 | 43 | transition: color 0.3s ease; 44 | 45 | &.storyfinder-highlighted { 46 | color: #AAAAAA; 47 | 48 | sf-entity { 49 | .typed-entity-highlighted(ORG, @ORG); 50 | .typed-entity-highlighted(LOC, @LOC); 51 | .typed-entity-highlighted(PER, @PER); 52 | .typed-entity-highlighted(OTH, @OTH); 53 | .typed-entity-highlighted(KEY, @KEY); 54 | } 55 | } 56 | 57 | /*border: 1px solid #0000FF; 58 | background-color: #EEEEEE;*/ 59 | 60 | .entity { 61 | //text-decoration: underline; 62 | //border-bottom: 1px dotted #0000FF; 63 | cursor: context-menu; 64 | white-space: nowrap; 65 | //color: #FF0000 !important; 66 | 67 | sf-marker { 68 | display: inline-block; 69 | //font: normal normal normal @fa-font-size-base/1 StoryFinderFontAwesome; // shortening font declaration 70 | font-size: 70%; // can't have font-size inherit on line above, so need to override 71 | text-rendering: auto; // optimizelegibility throws things off #1094 72 | -webkit-font-smoothing: antialiased; 73 | -moz-osx-font-smoothing: grayscale; 74 | transform: translate(0, 0); 75 | //content: @fa-var-info-circle; 76 | padding-left: 5px; 77 | 78 | &.internal-ref { 79 | &:before { 80 | //content: @fa-var-circle; 81 | } 82 | } 83 | 84 | &.external-ref { 85 | &:before { 86 | //content: @fa-var-external-link; 87 | } 88 | } 89 | } 90 | } 91 | 92 | sf-entity { 93 | text-shadow: 0px 0px 5px #FFFF00; 94 | .typed-entity(ORG, @ORG); 95 | .typed-entity(LOC, @LOC); 96 | .typed-entity(PER, @PER); 97 | .typed-entity(OTH, @OTH); 98 | .typed-entity(KEY, @KEY); 99 | } 100 | 101 | sf-entity.storyfinder-highlighted { 102 | //text-shadow: 0px 0px 5px #0074ff; 103 | } 104 | 105 | /*.entity:after { 106 | display: inline-block; 107 | font: normal normal normal @fa-font-size-base/1 StoryFinderFontAwesome; // shortening font declaration 108 | font-size: inherit; // can't have font-size inherit on line above, so need to override 109 | text-rendering: auto; // optimizelegibility throws things off #1094 110 | -webkit-font-smoothing: antialiased; 111 | -moz-osx-font-smoothing: grayscale; 112 | transform: translate(0, 0); 113 | content: @fa-var-info-circle; 114 | padding-left: 5px; 115 | }*/ 116 | 117 | 118 | &.storyfinder { 119 | position: absolute; 120 | right: 0; 121 | width: 40px; 122 | height: auto; 123 | top: 0px; 124 | bottom: 0px; 125 | background-color: #FFFFFF; 126 | box-shadow: -2px 2px 2px rgba(0,0,0,0.3); 127 | z-index: 99900; 128 | display: block; 129 | //@import "svg.less"; 130 | 131 | &.fixed { 132 | top: 0 !important; 133 | position: fixed; 134 | } 135 | 136 | sf-toolbar { 137 | .loading { 138 | width: 40px; 139 | height: 40px; 140 | font-size: 30px; 141 | vertical-align: middle; 142 | line-height: 40px; 143 | text-align: center; 144 | } 145 | 146 | .btn { 147 | height: 40px; 148 | border-left: none; 149 | border-right: none; 150 | line-height: 40px; 151 | vertical-align: middle; 152 | } 153 | } 154 | 155 | .articlemap { 156 | position: fixed; 157 | right: 40px; 158 | bottom: 0px; 159 | box-shadow: -2px 2px 2px rgba(0,0,0,0.3); 160 | 161 | img { 162 | max-width: 150px; 163 | height: 350px; 164 | } 165 | } 166 | } 167 | } 168 | 169 | .storyfinder-overlay { 170 | position: absolute; 171 | top: 0; 172 | left: 0; 173 | pointer-events: none; 174 | z-index: 999999; 175 | 176 | node { 177 | pointer-events: visiblePainted; 178 | } 179 | 180 | path { 181 | stroke: #999999; 182 | fill: none; 183 | } 184 | } 185 | 186 | sf-source { 187 | position: absolute; 188 | right: 130px; 189 | background-color: #FFFFFF; 190 | border: 1px solid #CCCCCC; 191 | padding: 3px; 192 | z-index: 999998; 193 | max-width: 120px; 194 | font-size: 80%; 195 | 196 | img { 197 | width: 120px; 198 | } 199 | } 200 | 201 | sf-marker-container { 202 | z-index: 999999; 203 | 204 | sf-marker { 205 | display: none; 206 | opacity: 0; 207 | transition: opacity 0.1s; 208 | 209 | .marker-above, .marker-below { 210 | display:none; 211 | } 212 | 213 | &.above, &.below { 214 | position: fixed; 215 | display: inline-block; 216 | opacity: 1; 217 | 218 | padding: 10px; 219 | background-color: rgba(0,0,0,0.8); 220 | color: #FFFFFF; 221 | transition: opacity 0.1s; 222 | } 223 | 224 | &.above { 225 | bottom: auto; 226 | top: 0; 227 | .marker-above { 228 | display: inline-block; 229 | } 230 | } 231 | 232 | &.below { 233 | top: auto; 234 | bottom: 0; 235 | .marker-below { 236 | display: inline-block; 237 | } 238 | } 239 | } 240 | } 241 | } -------------------------------------------------------------------------------- /webserver/models/ArticleEntity.js: -------------------------------------------------------------------------------- 1 | var DatasourceMysql = require('../datasources/mysql.js') 2 | , async = require('async') 3 | , _ = require('lodash') 4 | ; 5 | 6 | module.exports = function(db){ 7 | //console.log('ArticleEntity'); 8 | var name = 'ArticleEntity' 9 | , table = 'articles_entities' 10 | , datasource = new DatasourceMysql(db, name, table) 11 | ; 12 | 13 | function getCounts(entities, callback){ 14 | datasource.find('list', { 15 | fields: ['entity_id', 'sum(count) `total`'], 16 | conditions: { 17 | entity_id: entities, 18 | is_deleted: 0 19 | }, 20 | group: 'entity_id' 21 | }, callback); 22 | } 23 | 24 | this.getCounts = getCounts; 25 | 26 | function getInArticle(articleId, callback){ 27 | datasource.find('list', { 28 | fields: ['entity_id', 'sum(count) `total`'], 29 | conditions: { 30 | article_id: articleId, 31 | is_deleted: 0 32 | }, 33 | group: 'entity_id' 34 | }, callback); 35 | } 36 | 37 | this.getInArticle = getInArticle; 38 | 39 | function getDocumentfrequency(entityId, callback){ 40 | datasource.find('list', { 41 | fields: ['entity_id', 'count(*) `df`'], 42 | conditions: { 43 | entity_id: entityId, 44 | is_deleted: 0 45 | }, 46 | group: 'entity_id' 47 | }, callback); 48 | } 49 | 50 | this.getDocumentfrequency = getDocumentfrequency; 51 | 52 | function findArticlesWithEntity(entityId, callback){ 53 | datasource.find('list', { 54 | fields: ['article_id', 'id', 'entity_id', 'count'], 55 | conditions: { 56 | entity_id: entityId, 57 | is_deleted: 0 58 | } 59 | }, callback); 60 | } 61 | 62 | this.findArticlesWithEntity = findArticlesWithEntity; 63 | 64 | function reassign(/*target, source, changelogId, callback*/){ 65 | console.log('Reassign articleentity'); 66 | var targetId = arguments[0] 67 | , sourceId = arguments[1] 68 | , changelogId = arguments[2] 69 | , callback = arguments[arguments.length - 1] 70 | ; 71 | 72 | datasource.find('all', { 73 | fields: ['article_id', 'id', 'entity_id', 'count'], 74 | conditions: { 75 | entity_id: [targetId, sourceId], 76 | is_deleted: 0 77 | } 78 | }, (err, articles) => { 79 | if(err)return setImmediate(() => callback(err)); 80 | if(_.isEmpty(articles))return setImmediate(() => callback(null, changelogId)); 81 | 82 | var byId = {}; 83 | 84 | byId[targetId] = {}; 85 | byId[sourceId] = {}; 86 | 87 | articles.forEach(article => { 88 | if(article.entity_id == sourceId) 89 | byId[sourceId][article.article_id] = article; 90 | else 91 | byId[targetId][article.article_id] = article; 92 | }); 93 | 94 | async.forEachOf(byId[sourceId], (article, article_id, nextArticle) => { 95 | console.log(article_id); 96 | 97 | if(_.isUndefined(byId[targetId][article_id])){ 98 | console.log('reassign'); 99 | //Target is not connected with the article => move 100 | datasource.update(changelogId, { 101 | values: { 102 | entity_id: targetId 103 | }, 104 | conditions: { 105 | id: article.id 106 | }, 107 | limit: 1 108 | }, nextArticle); 109 | }else{ 110 | console.log('update'); 111 | //console.log(byId[targetId][article_id].id) 112 | //Target is already connected with the article => sum 113 | datasource.update(changelogId, { //Sum the count of the article relation of the target node 114 | values: { 115 | count: byId[targetId][article_id].count + article.count 116 | }, 117 | conditions: { 118 | id: byId[targetId][article_id].id 119 | }, 120 | limit: 1 121 | }, (err) => { 122 | if(err)return setImmediate(() => nextArticle(err)); 123 | 124 | console.log('Done Article'); 125 | 126 | datasource.update(changelogId, { //Delete the article relation of the source node 127 | values: { 128 | is_deleted: 1 129 | }, 130 | conditions: { 131 | id: article.id 132 | }, 133 | limit: 1 134 | }, nextArticle); 135 | }); 136 | } 137 | }, (err) => { 138 | if(err) 139 | return setImmediate(() => callback(err)); 140 | 141 | setImmediate(() => callback(null, changelogId)); 142 | }); 143 | }); 144 | } 145 | 146 | this.reassign = reassign; 147 | 148 | function add(memo, next){ 149 | async.eachOfSeries(memo.data.Entities, (entity, key, nextEntity) => { 150 | datasource.insertOrUpdate(memo.changelog_id, { 151 | values: { 152 | article_id: memo.data.Article.id, 153 | entity_id: entity.id, 154 | count: (memo.data.ngrams[entity.tokens - 1][entity.caption] || []).length, 155 | is_deleted: 0 156 | }, 157 | conditions: { 158 | article_id: memo.data.Article.id, 159 | entity_id: entity.id 160 | }, 161 | update: { 162 | count: (prevEntry, nextEntry) => { 163 | return parseInt(prevEntry) + parseInt(nextEntry) 164 | } 165 | } 166 | }, (err, insertId) => { 167 | if(err)return setImmediate(() => nextEntity(err)); 168 | 169 | //entity.id = insertId; 170 | setImmediate(nextEntity); 171 | }); 172 | }, (err) => { 173 | if(err)return setImmediate(() => next(err)); 174 | 175 | setImmediate(() => next(null, memo)); 176 | }); 177 | } 178 | 179 | this.add = add; 180 | 181 | function addByEntity(memo, next){ 182 | async.eachOfSeries(memo.Articles, (sentences, articleId, nextArticle) => { 183 | datasource.insertOrUpdate(memo.changelog_id, { 184 | values: { 185 | article_id: articleId, 186 | entity_id: memo.Entity.id, 187 | count: sentences.length, 188 | is_deleted: 0 189 | }, 190 | conditions: { 191 | article_id: articleId, 192 | entity_id: memo.Entity.id 193 | }, 194 | update: { 195 | count: (prevEntry, nextEntry) => { 196 | return parseInt(prevEntry.count) + parseInt(nextEntry.count) 197 | } 198 | } 199 | }, (err, insertId) => { 200 | if(err)return setImmediate(() => nextArticle(err)); 201 | 202 | setImmediate(nextArticle); 203 | }); 204 | }, (err) => { 205 | if(err)return setImmediate(() => next(err)); 206 | 207 | setImmediate(() => next(null, memo)); 208 | }); 209 | } 210 | 211 | this.addByEntity = addByEntity; 212 | } 213 | -------------------------------------------------------------------------------- /webserver/public/js/actions/StoryfinderActions.js: -------------------------------------------------------------------------------- 1 | import * as types from '../constants/ActionTypes'; 2 | 3 | export function toRelation(entity1_id, entity2_id) { 4 | return { 5 | type: types.TO_RELATION, 6 | entity1_id: entity1_id, 7 | entity2_id: entity2_id 8 | }; 9 | } 10 | 11 | export function showRelation(entity1_id, entity2_id) { 12 | return function(dispatch){ 13 | dispatch(requestRelation(entity1_id, entity2_id)); 14 | 15 | return fetch('/Relations/' + entity1_id + '/' + entity2_id, { 16 | credentials: 'same-origin' 17 | }) 18 | .then(response => response.json()) 19 | .then(json => { 20 | dispatch(receiveRelation(json)); 21 | }).catch(err => { 22 | dispatch(receiveRelation({})); 23 | }) 24 | } 25 | } 26 | 27 | export function requestRelation(entity1_id, entity2_id){ 28 | return { 29 | type: types.REQUEST_RELATION, 30 | entity1_id: entity1_id, 31 | entity2_id: entity2_id 32 | }; 33 | } 34 | 35 | export function receiveRelation(data){ 36 | return { 37 | type: types.RECEIVE_RELATION, 38 | data: data 39 | }; 40 | } 41 | 42 | export function toGraph(){ 43 | return { 44 | type: types.TO_GRAPH 45 | }; 46 | } 47 | 48 | export function showGraph(){ 49 | return { 50 | type: types.SHOW_GRAPH 51 | }; 52 | } 53 | 54 | export function toLocalgraph(site_id){ 55 | return { 56 | type: types.TO_LOCALGRAPH, 57 | site_id: site_id 58 | }; 59 | } 60 | 61 | export function toGroupgraph(sites){ 62 | return { 63 | type: types.TO_GROUPGRAPH, 64 | sites: sites 65 | }; 66 | } 67 | 68 | export function showGroupgraph(sites, articles){ 69 | return { 70 | type: types.SHOW_GROUPGRAPH, 71 | sites: sites, 72 | articles: articles 73 | } 74 | } 75 | 76 | export function showLocalgraph(site, article){ 77 | return { 78 | type: types.SHOW_LOCALGRAPH, 79 | site: site, 80 | article: article 81 | } 82 | } 83 | 84 | export function initializeGlobal() { 85 | return function(dispatch){ 86 | dispatch(requestGlobal()); 87 | 88 | return fetch('/Globalgraphs') 89 | .then(response => response.json()) 90 | .then(json => { 91 | dispatch(receiveGlobal(json)); 92 | }).catch(err => { 93 | dispatch(receiveGlobal({})); 94 | }) 95 | } 96 | } 97 | 98 | export function requestGlobal(){ 99 | return { 100 | type: types.REQUEST_GLOBAL 101 | }; 102 | } 103 | 104 | export function receiveGlobal(data){ 105 | return { 106 | type: types.RECEIVE_GLOBAL, 107 | data: data 108 | }; 109 | } 110 | 111 | export function deleteNode(userId, id) { 112 | return function(dispatch){ 113 | dispatch(requestDeleteNode()); 114 | 115 | return fetch('/Entities/' + id, { 116 | method: 'DELETE', 117 | credentials: 'same-origin' 118 | }) 119 | .then(response => response.json()) 120 | .then(json => { 121 | dispatch(receiveDeleteNode(json)); 122 | }).catch(err => { 123 | dispatch(receiveDeleteNode({})); 124 | }) 125 | } 126 | } 127 | 128 | export function requestDeleteNode(){ 129 | return { 130 | type: types.REQUEST_DELETE_NODE 131 | }; 132 | } 133 | 134 | export function receiveDeleteNode(data){ 135 | return { 136 | type: types.RECEIVE_DELETE_NODE, 137 | data: data 138 | }; 139 | } 140 | 141 | export function toGlobal(){ 142 | return { 143 | type: types.TO_GLOBAL 144 | } 145 | } 146 | 147 | export function showGlobal(){ 148 | return { 149 | type: types.SHOW_GLOBAL 150 | } 151 | } 152 | 153 | export function expandNode(nodeId) { 154 | return function(dispatch){ 155 | dispatch(requestNeighbours(nodeId)); 156 | 157 | return fetch('http://127.0.0.1:3055/1/nodes/' + nodeId + '/neighbours') 158 | .then(response => response.json()) 159 | .then(json => { 160 | dispatch(receiveNeighbours(nodeId, json)); 161 | }).catch(err => { 162 | dispatch(receiveNeighbours(nodeId, {})); 163 | }) 164 | } 165 | } 166 | 167 | export function requestNeighbours(nodeId){ 168 | return { 169 | type: types.REQUEST_NEIGHBOURS, 170 | node: nodeId 171 | } 172 | } 173 | 174 | export function receiveNeighbours(nodeId, data){ 175 | return { 176 | type: types.RECEIVE_NEIGHBOURS, 177 | node: nodeId, 178 | data: data 179 | } 180 | } 181 | 182 | export function createNode(data){ 183 | return { 184 | type: types.CREATE_NODE, 185 | data: data 186 | } 187 | } 188 | 189 | export function saveNode(nodeData){ 190 | return function(dispatch){ 191 | dispatch(requestSaveNode()); 192 | 193 | return fetch('/Entities', { 194 | method: 'PUT', 195 | headers: { 196 | 'Accept': 'application/json', 197 | 'Content-Type': 'application/json' 198 | }, 199 | body: JSON.stringify(nodeData), 200 | credentials: 'same-origin' 201 | }) 202 | .then(response => response.json()) 203 | .then(json => { 204 | dispatch(receiveSaveNode(json)); 205 | }).catch(err => { 206 | dispatch(receiveSaveNode({})); 207 | }) 208 | } 209 | } 210 | 211 | export function requestSaveNode(){ 212 | return { 213 | type: types.REQUEST_SAVE_NODE 214 | } 215 | } 216 | 217 | export function receiveSaveNode(data){ 218 | return { 219 | type: types.RECEIVE_SAVE_NODE, 220 | data: data 221 | } 222 | } 223 | 224 | export function createRelation(entity1, entity2, label){ 225 | return function(dispatch){ 226 | dispatch(requestCreateRelation()); 227 | 228 | return fetch('/Relations/' + entity1 + '/' + entity2, { 229 | method: 'PUT', 230 | headers: { 231 | 'Accept': 'application/json', 232 | 'Content-Type': 'application/json' 233 | }, 234 | body: JSON.stringify({ 235 | label: label 236 | }), 237 | credentials: 'same-origin' 238 | }) 239 | .then(response => response.json()) 240 | .then(json => { 241 | dispatch(receiveCreateRelation(json)); 242 | }).catch(err => { 243 | dispatch(receiveCreateRelation({})); 244 | }) 245 | } 246 | } 247 | 248 | export function requestCreateRelation(){ 249 | return { 250 | type: types.REQUEST_CREATE_RELATION 251 | } 252 | } 253 | 254 | export function receiveCreateRelation(data){ 255 | return { 256 | type: types.RECEIVE_CREATE_RELATION, 257 | data: data 258 | } 259 | } 260 | 261 | /*export function selectLayer(path, layerId) { 262 | return function(dispatch){ 263 | path = path.push(layerId); 264 | 265 | dispatch(requestLayers(path)); 266 | 267 | return fetch('http://127.0.0.1:3000/Layers.json?path=' + path.toJS().join('/') + '&fields=data') 268 | .then(response => response.json()) 269 | .then(json => { 270 | dispatch(receiveLayers(path, json.data)); 271 | }) 272 | } 273 | }*/ -------------------------------------------------------------------------------- /webserver/server.js: -------------------------------------------------------------------------------- 1 | var express = require('express') 2 | , app = express() 3 | , http = require('http').Server(app) 4 | , io = require('socket.io')(http) 5 | , bodyParser = require('body-parser') 6 | , cookieSession = require('cookie-session') 7 | , methodOverride = require('method-override') 8 | , serveStatic = require('serve-static') 9 | , port = 3055 10 | , mysql = require('mysql') 11 | , connection = mysql.createConnection({ 12 | host : process.env.MYSQL_HOST || "mysql", 13 | port : process.env.MYSQL_PORT || 3306, 14 | user : process.env.MYSQL_USER || 'storyfinder', 15 | password : process.env.MYSQL_PASSWORD || 'storyfinder', 16 | database : process.env.MYSQL_DATABASE || 'storyfinder', 17 | charset: "utf8_general_ci" 18 | }) 19 | , fs = require('fs') 20 | , handleVisit = require('./libs/handle_visit.js') 21 | , async = require('async') 22 | , _ = require('lodash') 23 | , passport = require('passport') 24 | , BasicStrategy = require('passport-http').BasicStrategy 25 | , LocalStrategy = require('passport-local').Strategy 26 | , User = new (require('./models/User.js'))(connection) 27 | , ensureLoggedIn = require('connect-ensure-login').ensureLoggedIn 28 | , exphbs = require('express-handlebars') 29 | , tables = {'articles': true,'articles_entities': true,'articles_tokens': true,'changelogs': true,'changelogs_updates': true,'collections': true,'entities': true,'entities_sentences': true,'log_entities': true,'ngrams': true,'relations': true,'relations_sentences': true,'relationtypes': true,'sentences': true,'sites': true,'tokens': true,'users': true,'visits': true} 30 | ; 31 | 32 | function startServer(){ 33 | http.listen(port, function () { 34 | console.log('Storyfinder listening on port ' + port); 35 | }); 36 | } 37 | 38 | /* Write CHROME_ID to file */ 39 | var fs = require('fs') 40 | fs.readFile("public/js/storyfinder.js", 'utf8', function (err,data) { 41 | if (err) { 42 | return console.log(err); 43 | } 44 | var result = data.replace(/INSERT_CHROME_PLUGIN_ID_HERE/g, process.env.CHROME_ID); 45 | 46 | fs.writeFile("public/js/storyfinder.js", result, 'utf8', function (err) { 47 | if (err) return console.log(err); 48 | }); 49 | }); 50 | 51 | /* 52 | Initialise database. 53 | */ 54 | connection.query('SHOW TABLES', (err, result, fields) => { 55 | if(err){ 56 | throw err; 57 | return; 58 | } 59 | 60 | var fieldName = fields[0].name; 61 | 62 | for(var row of result) { 63 | var tbl = row[fieldName]; 64 | 65 | if(typeof tables[tbl] !== 'undefined') 66 | delete tables[tbl]; 67 | } 68 | 69 | if(!_.isEmpty(tables)){ 70 | console.log('Some database tables are missing. Creating tables ' + _.keys(tables).join("\n")); 71 | fs.readFile('./data/sql/schema.sql', (err, content) => { 72 | if(err){ 73 | throw err; 74 | return; 75 | } 76 | 77 | var queries = content.toString().split("\n\n"); 78 | 79 | async.each(queries, (query, nextQuery) => { 80 | connection.query(query, nextQuery); 81 | }, startServer); 82 | }); 83 | }else{ 84 | setImmediate(startServer); 85 | } 86 | }); 87 | 88 | /* 89 | Authentication 90 | */ 91 | passport.use(new BasicStrategy( 92 | function(username, password, done){ 93 | User.verify(username, password, function(err, isValid, user){ 94 | if (err) { return done(err); } 95 | if (!isValid) { 96 | return done(null, false, { message: 'Incorrect username or password.' }); 97 | } 98 | return done(null, user); 99 | }); 100 | } 101 | )); 102 | 103 | passport.use(new LocalStrategy( 104 | function(username, password, done){ 105 | User.verify(username, password, function(err, isValid, user){ 106 | if (err) { 107 | console.log('Passport ERROR'); 108 | return done(err); 109 | } 110 | if (!isValid) { 111 | console.log('Passport Incorrect username or password'); 112 | return done(null, false, { message: 'Incorrect username or password.' }); 113 | } 114 | console.log('Passport success!'); 115 | return done(null, user); 116 | }); 117 | } 118 | )); 119 | 120 | passport.serializeUser(function(user, done) { 121 | if(user == null) 122 | done(null, null); 123 | else 124 | done(null, user.id); 125 | }); 126 | 127 | passport.deserializeUser(function(id, done) { 128 | User.findById(id, function(err, user) { 129 | console.log('User deserialized'); 130 | done(err, user); 131 | }); 132 | }); 133 | 134 | function loggedInHTTP(req, res, next){ 135 | if (!req.isAuthenticated || !req.isAuthenticated()) { 136 | passport.authenticate('basic', {session: true})(req, res, next); 137 | }else{ 138 | next(); 139 | } 140 | } 141 | 142 | // parse application/json 143 | app.use(bodyParser.json({limit: '50mb'})) 144 | app.use(bodyParser.urlencoded({ extended: false })) 145 | 146 | app.use(cookieSession({secret: process.env.COOKIE_SECRET, maxAge: 24 * 60 * 60 * 1000 * 365})); 147 | 148 | //MethodOverride 149 | app.use(methodOverride('X-HTTP-Method-Override')) 150 | app.use(methodOverride('_method')) 151 | 152 | //Serve static files 153 | app.use(serveStatic(__dirname + '/public')) 154 | app.use(passport.initialize()); 155 | app.use(passport.session()); 156 | 157 | var hbs = exphbs.create({ 158 | // Specify helpers which are only registered on this instance. 159 | helpers: { 160 | style: function () { return fs.readFileSync(__dirname + '/public/css/bootstrap.css').toString(); } 161 | }, 162 | defaultLayout: 'main' 163 | }); 164 | 165 | app.engine('handlebars', hbs.engine); 166 | 167 | app.set('view engine', 'handlebars'); 168 | 169 | var UsersController = new (require('./controllers/UsersController.js'))(connection, app, passport) 170 | , GraphsController = new (require('./controllers/GraphsController.js'))(connection, app, passport) 171 | , SitesController = new (require('./controllers/SitesController.js'))(connection, app, passport, io) 172 | , EntitiesController = new (require('./controllers/EntitiesController.js'))(connection, app, passport, io) 173 | , ArticlesController = new (require('./controllers/ArticlesController.js'))(connection, app, passport) 174 | , RelationsController = new (require('./controllers/RelationsController.js'))(connection, app, passport) 175 | ; 176 | 177 | app.get('/', ensureLoggedIn((process.env.PATH_PREFIX || '/') + 'login'), function (req, res) { 178 | fs.readFile(__dirname + '/public/css/bootstrap.css', function(err, style){ 179 | if(err){ 180 | console.log(err); 181 | return; 182 | } 183 | fs.readFile(__dirname + '/public/_index.html', function(err, html){ 184 | if(err){ 185 | console.log(err); 186 | return; 187 | } 188 | res.send(html.toString().replace(/\{\{style\}\}/, style.toString())); 189 | }); 190 | }); 191 | }); 192 | 193 | app.get('/reseteval', function (req, res) { 194 | var t = ['articles', 195 | 'articles_entities', 196 | 'articles_tokens', 197 | 'changelogs', 198 | 'changelogs_updates', 199 | 'entities', 200 | 'entities_sentences', 201 | 'log_entities', 202 | 'ngrams', 203 | 'relations', 204 | 'relations_sentences', 205 | 'relationtypes', 206 | 'sentences', 207 | 'sites', 208 | 'tokens']; 209 | 210 | for(var i = 0;i < t.length; i++) 211 | connection.query('TRUNCATE ' + t[i]); 212 | 213 | res.send('ok'); 214 | }); 215 | -------------------------------------------------------------------------------- /webserver/public/js/libs/shortestpaths.js: -------------------------------------------------------------------------------- 1 | /// 2 | /** 3 | * @module shortestpaths 4 | */ 5 | var cola; 6 | (function (cola) { 7 | var shortestpaths; 8 | (function (shortestpaths) { 9 | var Neighbour = (function () { 10 | function Neighbour(id, distance) { 11 | this.id = id; 12 | this.distance = distance; 13 | } 14 | return Neighbour; 15 | })(); 16 | var Node = (function () { 17 | function Node(id) { 18 | this.id = id; 19 | this.neighbours = []; 20 | } 21 | return Node; 22 | })(); 23 | var QueueEntry = (function () { 24 | function QueueEntry(node, prev, d) { 25 | this.node = node; 26 | this.prev = prev; 27 | this.d = d; 28 | } 29 | return QueueEntry; 30 | })(); 31 | /** 32 | * calculates all-pairs shortest paths or shortest paths from a single node 33 | * @class Calculator 34 | * @constructor 35 | * @param n {number} number of nodes 36 | * @param es {Edge[]} array of edges 37 | */ 38 | var Calculator = (function () { 39 | function Calculator(n, es, getSourceIndex, getTargetIndex, getLength) { 40 | this.n = n; 41 | this.es = es; 42 | this.neighbours = new Array(this.n); 43 | var i = this.n; 44 | while (i--) 45 | this.neighbours[i] = new Node(i); 46 | i = this.es.length; 47 | while (i--) { 48 | var e = this.es[i]; 49 | var u = getSourceIndex(e), v = getTargetIndex(e); 50 | var d = getLength(e); 51 | this.neighbours[u].neighbours.push(new Neighbour(v, d)); 52 | this.neighbours[v].neighbours.push(new Neighbour(u, d)); 53 | } 54 | } 55 | /** 56 | * compute shortest paths for graph over n nodes with edges an array of source/target pairs 57 | * edges may optionally have a length attribute. 1 is the default. 58 | * Uses Johnson's algorithm. 59 | * 60 | * @method DistanceMatrix 61 | * @return the distance matrix 62 | */ 63 | Calculator.prototype.DistanceMatrix = function () { 64 | var D = new Array(this.n); 65 | for (var i = 0; i < this.n; ++i) { 66 | D[i] = this.dijkstraNeighbours(i); 67 | } 68 | return D; 69 | }; 70 | /** 71 | * get shortest paths from a specified start node 72 | * @method DistancesFromNode 73 | * @param start node index 74 | * @return array of path lengths 75 | */ 76 | Calculator.prototype.DistancesFromNode = function (start) { 77 | return this.dijkstraNeighbours(start); 78 | }; 79 | Calculator.prototype.PathFromNodeToNode = function (start, end) { 80 | return this.dijkstraNeighbours(start, end); 81 | }; 82 | // find shortest path from start to end, with the opportunity at 83 | // each edge traversal to compute a custom cost based on the 84 | // previous edge. For example, to penalise bends. 85 | Calculator.prototype.PathFromNodeToNodeWithPrevCost = function (start, end, prevCost) { 86 | var q = new PriorityQueue(function (a, b) { return a.d <= b.d; }), u = this.neighbours[start], qu = new QueueEntry(u, null, 0), visitedFrom = {}; 87 | q.push(qu); 88 | while (!q.empty()) { 89 | qu = q.pop(); 90 | u = qu.node; 91 | if (u.id === end) { 92 | break; 93 | } 94 | var i = u.neighbours.length; 95 | while (i--) { 96 | var neighbour = u.neighbours[i], v = this.neighbours[neighbour.id]; 97 | // don't double back 98 | if (qu.prev && v.id === qu.prev.node.id) 99 | continue; 100 | // don't retraverse an edge if it has already been explored 101 | // from a lower cost route 102 | var viduid = v.id + ',' + u.id; 103 | if (viduid in visitedFrom && visitedFrom[viduid] <= qu.d) 104 | continue; 105 | var cc = qu.prev ? prevCost(qu.prev.node.id, u.id, v.id) : 0, t = qu.d + neighbour.distance + cc; 106 | // store cost of this traversal 107 | visitedFrom[viduid] = t; 108 | q.push(new QueueEntry(v, qu, t)); 109 | } 110 | } 111 | var path = []; 112 | while (qu.prev) { 113 | qu = qu.prev; 114 | path.push(qu.node.id); 115 | } 116 | return path; 117 | }; 118 | Calculator.prototype.dijkstraNeighbours = function (start, dest) { 119 | if (dest === void 0) { dest = -1; } 120 | var q = new PriorityQueue(function (a, b) { return a.d <= b.d; }), i = this.neighbours.length, d = new Array(i); 121 | while (i--) { 122 | var node = this.neighbours[i]; 123 | node.d = i === start ? 0 : Number.POSITIVE_INFINITY; 124 | node.q = q.push(node); 125 | } 126 | while (!q.empty()) { 127 | // console.log(q.toString(function (u) { return u.id + "=" + (u.d === Number.POSITIVE_INFINITY ? "\u221E" : u.d.toFixed(2) )})); 128 | var u = q.pop(); 129 | d[u.id] = u.d; 130 | if (u.id === dest) { 131 | var path = []; 132 | var v = u; 133 | while (typeof v.prev !== 'undefined') { 134 | path.push(v.prev.id); 135 | v = v.prev; 136 | } 137 | return path; 138 | } 139 | i = u.neighbours.length; 140 | while (i--) { 141 | var neighbour = u.neighbours[i]; 142 | var v = this.neighbours[neighbour.id]; 143 | var t = u.d + neighbour.distance; 144 | if (u.d !== Number.MAX_VALUE && v.d > t) { 145 | v.d = t; 146 | v.prev = u; 147 | q.reduceKey(v.q, v, function (e, q) { return e.q = q; }); 148 | } 149 | } 150 | } 151 | return d; 152 | }; 153 | return Calculator; 154 | })(); 155 | shortestpaths.Calculator = Calculator; 156 | })(shortestpaths = cola.shortestpaths || (cola.shortestpaths = {})); 157 | })(cola || (cola = {})); 158 | //# sourceMappingURL=shortestpaths.js.map -------------------------------------------------------------------------------- /webserver/public/js/reducers/storyfinder.js: -------------------------------------------------------------------------------- 1 | import Immutable from 'immutable'; 2 | import * as types from '../constants/ActionTypes'; 3 | 4 | const initialState = Immutable.Map({ 5 | currentSite: null, 6 | focusedNode: null, 7 | selectedNode: null, 8 | isFetching: false, 9 | state: 'graph', 10 | graph: null 11 | /*groups: Immutable.Map({ 12 | 0: Immutable.Map({ 13 | name: 'No Group', 14 | children: Immutable.List([]) 15 | }) 16 | })*/ 17 | }); 18 | 19 | /*function receiveLayers(state, path, layers){ 20 | var parentId = path.last(); 21 | 22 | var layerIds = new Immutable.List(); 23 | layers.forEach((layer) => { 24 | layerIds = layerIds.push(layer.id); 25 | state = state.setIn(['layers', layer.id], new Immutable.fromJS(layer)); 26 | }); 27 | 28 | state = state.set('isFetching', false); 29 | if(path.size == 1){ 30 | state = state.setIn(['groups', parentId, 'children'], layerIds); 31 | }else{ 32 | state = state.setIn(['layers', parentId, 'children'], layerIds); 33 | } 34 | 35 | return state; 36 | }*/ 37 | 38 | function toRelation(state, entity1_id, entity2_id){ 39 | state = state.set('state', state.get('state') + '-to-relation'); 40 | state = state.set('entity1_id', entity1_id); 41 | state = state.set('entity2_id', entity2_id); 42 | return state; 43 | } 44 | 45 | function showRelation(state, entity1_id, entity2_id){ 46 | state = state.set('state', 'relation'); 47 | state = state.set('entity1_id', entity1_id); 48 | state = state.set('entity2_id', entity2_id); 49 | return state; 50 | } 51 | 52 | function receiveRelation(state, data){ 53 | state = state.set('state', 'relation'); 54 | state = state.set('isFetching', false); 55 | state = state.set('relation', data); 56 | return state; 57 | } 58 | 59 | function requestRelation(state, entity1_id, entity2_id){ 60 | state = state.set('state', 'relation'); 61 | state = state.set('isFetching', true); 62 | return state; 63 | } 64 | 65 | function requestGlobal(state){ 66 | state = state.set('state', 'requesting-graph'); 67 | return state; 68 | } 69 | 70 | function receiveGlobal(state, graph){ 71 | state = state.set('state', 'graph'); 72 | /*var nodes = {}; 73 | graph.nodes.forEach(function(node){ 74 | nodes[node.id] = node; 75 | }); 76 | graph.nodes = nodes;*/ 77 | 78 | state = state.set('graph', Immutable.fromJS(graph)); 79 | return state; 80 | } 81 | 82 | function toGraph(state){ 83 | state = state.set('state', state.get('state') + '-to-graph'); 84 | return state; 85 | } 86 | 87 | function showGraph(state){ 88 | state = state.set('state', 'graph'); 89 | return state; 90 | } 91 | 92 | function toLocalgraph(state, site_id){ 93 | state = state.set('state', state.get('state') + '-to-localgraph'); 94 | state = state.set('site_id', site_id); 95 | state = state.set('site', null); 96 | state = state.set('sites', null); 97 | state = state.set('site_ids', null); 98 | state = state.set('articles', null); 99 | state = state.set('groupsize', null); 100 | return state; 101 | } 102 | 103 | function toGroupgraph(state, sites){ 104 | state = state.set('state', state.get('state') + '-to-groupgraph'); 105 | state = state.set('site_ids', Immutable.fromJS(sites)); 106 | state = state.set('site_id', null); 107 | state = state.set('article', null); 108 | state = state.set('site', null); 109 | return state; 110 | } 111 | 112 | function showLocalgraph(state, site, article){ 113 | state = state.set('state', 'localgraph'); 114 | state = state.set('site', Immutable.fromJS(site)); 115 | state = state.set('article', Immutable.fromJS(article)); 116 | return state; 117 | } 118 | 119 | function showGroupgraph(state, sites, articles){ 120 | state = state.set('state', 'groupgraph'); 121 | state = state.set('sites', Immutable.fromJS(sites)); 122 | state = state.set('articles', Immutable.fromJS(articles)); 123 | state = state.set('groupsize', state.get('site_ids').toJSON().length); 124 | return state; 125 | } 126 | 127 | function toGlobal(state){ 128 | state = state.set('state', state.get('state') + '-to-global'); 129 | state = state.set('site_id', null); 130 | state = state.set('site_ids', null); 131 | state = state.set('site', null); 132 | state = state.set('article', null); 133 | state = state.set('sites', null); 134 | state = state.set('articles', null); 135 | state = state.set('groupsize', null); 136 | state = state.set('sites', null); 137 | return state; 138 | } 139 | 140 | function showGlobal(state){ 141 | state = state.set('state', 'graph'); 142 | return state; 143 | } 144 | 145 | function requestNeighbours(state, nodeId){ 146 | state = state.set('state', 'request-neighbours'); 147 | return state; 148 | } 149 | 150 | function receiveNeighbours(state, nodeId, neighbours){ 151 | state = state.set('state', 'graph'); 152 | state = state.setIn(['graph', 'nodes', nodeId, 'expanded'], true); 153 | state = state.mergeIn(['graph', 'nodes'], neighbours.nodes); 154 | state = state.mergeIn(['graph', 'links'], neighbours.links); 155 | return state; 156 | } 157 | 158 | function createNode(state, data){ 159 | state = state.set('state', 'create'); 160 | state = state.set('nodedata', Immutable.fromJS(data)); 161 | return state; 162 | } 163 | 164 | function requestSaveNode(state){ 165 | state = state.set('state', 'request-save-node'); 166 | return state; 167 | } 168 | 169 | function receiveSaveNode(state, nodeData){ 170 | state = state.set('state', 'receive-save-node'); 171 | state = state.set('node', Immutable.fromJS(nodeData)); 172 | return state; 173 | } 174 | 175 | function requestCreateRelation(state){ 176 | state = state.set('state', 'request-create-relation'); 177 | return state; 178 | } 179 | 180 | function receiveCreateRelation(state, data){ 181 | state = state.set('state', 'receive-create-relation'); 182 | state = state.set('relation', Immutable.fromJS(data)); 183 | return state; 184 | } 185 | 186 | export default function layerlist(state = initialState, action) { 187 | switch (action.type) { 188 | case 'TO_RELATION': 189 | return toRelation(state, action.entity1_id, action.entity2_id); 190 | /*case 'SHOW_RELATION': 191 | return showRelation(state, action.entity1_id, action.entity2_id);*/ 192 | case 'REQUEST_RELATION': 193 | return requestRelation(state, action.entity1_id, action.entity2_id); 194 | case 'RECEIVE_RELATION': 195 | return receiveRelation(state, action.data); 196 | case 'TO_GRAPH': 197 | return toGraph(state); 198 | case 'SHOW_GRAPH': 199 | return showGraph(state); 200 | case 'TO_LOCALGRAPH': 201 | return toLocalgraph(state, action.site_id); 202 | case 'TO_GROUPGRAPH': 203 | return toGroupgraph(state, action.sites); 204 | case 'SHOW_LOCALGRAPH': 205 | return showLocalgraph(state, action.site, action.article); 206 | case 'SHOW_GROUPGRAPH': 207 | return showGroupgraph(state, action.sites, action.articles); 208 | case 'TO_GLOBAL': 209 | return toGlobal(state); 210 | case 'SHOW_GLOBAL': 211 | return showGlobal(state); 212 | case 'REQUEST_GLOBAL': 213 | return requestGlobal(state); 214 | case 'RECEIVE_GLOBAL': 215 | return receiveGlobal(state, action.data); 216 | case 'REQUEST_NEIGHBOURS': 217 | return requestNeighbours(state, action.node); 218 | case 'RECEIVE_NEIGHBOURS': 219 | return receiveNeighbours(state, action.node, action.data); 220 | case 'CREATE_NODE': 221 | return createNode(state, action.data); 222 | case 'REQUEST_SAVE_NODE': 223 | return requestSaveNode(state); 224 | case 'RECEIVE_SAVE_NODE': 225 | return receiveSaveNode(state, action.data); 226 | case 'REQUEST_CREATE_RELATION': 227 | return requestCreateRelation(state); 228 | case 'RECEIVE_CREATE_RELATION': 229 | return receiveCreateRelation(state, action.data); 230 | /*case 'SELECT_SITE': 231 | return selectSite(state, action.siteId); 232 | case 'SELECT_NODE': 233 | return selectNode(state, action.layerId); 234 | case 'RECEIVE_LAYERS': 235 | return receiveLayers(state, action.path, action.layers); 236 | break; 237 | case 'REQUEST_LAYERS': 238 | return requestLayers(state, action.path); 239 | break; 240 | case 'UP_LAYER': 241 | return upLayer(state);*/ 242 | } 243 | return state; 244 | } -------------------------------------------------------------------------------- /webserver/libs/globalgraph.js: -------------------------------------------------------------------------------- 1 | var async = require('async') 2 | , _ = require('lodash') 3 | , Pagerank = require('pagerank-js') 4 | ; 5 | 6 | module.exports = function(app, connection){ 7 | app.get('/:userId/globalgraph', function (req, res) { 8 | var userId = req.params.userId 9 | , siteId = req.params.siteId 10 | , sites = null 11 | , articles = null 12 | , entities = null 13 | , links = null 14 | , renderNodes = [] 15 | , renderLinks = [] 16 | , linksById = {} 17 | , nodesInRenderGraph = {} 18 | ; 19 | 20 | async.series([ 21 | function(callback){ 22 | connection.query('SELECT id, url, title, host, favicon, last_visited FROM sites WHERE user_id=? and is_deleted=0', [userId], function(err, results, fields){ 23 | if(err){ 24 | return callback(err); 25 | } 26 | 27 | sites = results; 28 | setImmediate(callback); 29 | }); 30 | }, 31 | function(callback){ 32 | connection.query('SELECT id, text FROM articles WHERE site_id IN (' + _.map(sites, 'id').join(',') + ') and is_deleted=0', [], function(err, results, fields){ 33 | if(err){ 34 | return callback(err); 35 | } 36 | 37 | if(_.isNull(results) || results.length == 0){ 38 | setImmediate(callback); 39 | return; 40 | } 41 | 42 | articles = results; 43 | setImmediate(callback); 44 | }); 45 | }, 46 | function(callback){ 47 | if(_.isUndefined(articles) || _.isNull(articles)){ 48 | setImmediate(callback); 49 | return; 50 | } 51 | 52 | connection.query('SELECT entities.id, entities.caption, entities.type, SUM(articles_entities.count) `count` FROM entities INNER JOIN articles_entities ON (articles_entities.entity_id=entities.id) WHERE articles_entities.article_id IN (' + _.map(articles, 'id').join(',') + ') and articles_entities.is_deleted=0 and entities.is_deleted=0 GROUP BY entities.id', [], function(err, results, fields){ 53 | if(err){ 54 | return callback(err); 55 | } 56 | 57 | entities = results; 58 | setImmediate(callback); 59 | 60 | }); 61 | }, 62 | function(callback){ 63 | var entity_ids = _.map(entities, 'id'); 64 | 65 | if(entity_ids.length == 0){ 66 | setImmediate(callback); 67 | return; 68 | } 69 | 70 | connection.query('SELECT relations.id, relations.entity1_id, relations.entity2_id, relations.relationtype_id, relations.direction, relations.user_generated, relationtypes.label FROM relations LEFT JOIN relationtypes ON (relations.relationtype_id = relationtypes.id) WHERE (relations.entity1_id IN (' + entity_ids.join(',') + ') or relations.entity2_id IN (' + entity_ids.join(',') + ')) and relations.is_deleted=0', function(err, results, fields){ 71 | if(err){ 72 | return callback(err); 73 | } 74 | 75 | links = results; 76 | setImmediate(callback); 77 | }) 78 | }, 79 | function(callback){ 80 | var linkProb = 0.85 //high numbers are more stable 81 | , tolerance = 0.0001 //sensitivity for accuracy of convergence. 82 | , entityToIdx = {} 83 | , nodesForPagerank = [] 84 | , linksByNode = {} 85 | ; 86 | 87 | for(var i = 0;i < entities.length; i++) 88 | entityToIdx[entities[i].id] = i; 89 | 90 | for(var i = 0;i < links.length; i++){ 91 | var el = [entityToIdx[links[i].entity1_id], entityToIdx[links[i].entity2_id]]; 92 | 93 | if(_.isUndefined(linksByNode[el[0]])) 94 | linksByNode[el[0]] = []; 95 | linksByNode[el[0]].push(el[1]); 96 | 97 | if(_.isUndefined(linksByNode[el[1]])) 98 | linksByNode[el[1]] = []; 99 | linksByNode[el[1]].push(el[0]); 100 | } 101 | 102 | for(var i = 0;i < entities.length; i++){ 103 | if(_.isUndefined(linksByNode[i])) 104 | nodesForPagerank.push([]); 105 | else 106 | nodesForPagerank.push(linksByNode[i]); 107 | } 108 | 109 | Pagerank(nodesForPagerank, linkProb, tolerance, function (err, res) { 110 | if (err) return callback(err) 111 | 112 | var max = 0 113 | , min = 1 114 | ; 115 | 116 | for(var i = 0;i < res.length; i++){ 117 | if(res[i] > max)max = res[i]; 118 | if(res[i] < min)min = res[i]; 119 | } 120 | 121 | //Skalierung zwischen 0 und 1 vornehmen 122 | for(var i = 0;i < res.length; i++){ 123 | var idx = i; 124 | 125 | if(min == max) 126 | entities[idx].pageRank = 0.5; 127 | else 128 | entities[idx].pageRank = (res[i] - min) / (max - min); 129 | } 130 | 131 | setImmediate(callback); 132 | }); 133 | }, 134 | function(callback){ 135 | var maxTopNodes = 10 136 | , maxNeighbours = 2 137 | , linksByNode = {} 138 | ; 139 | 140 | entities.sort(function(a, b){ 141 | return b.pageRank - a.pageRank; 142 | }); 143 | 144 | //Top 20 Knoten 145 | for(var i = 0; i < Math.min(maxTopNodes, entities.length); i++){ 146 | entities[i].isTopNode = true; 147 | //renderNodes[entities[i].id] = entities[i]; 148 | renderNodes.push(entities[i]); 149 | nodesInRenderGraph[i] = entities[i]; 150 | } 151 | 152 | var entityToIdx = {} 153 | , nodesForPagerank = [] 154 | ; 155 | 156 | for(var i = 0;i < entities.length; i++){ 157 | entityToIdx[entities[i].id * 1] = i; 158 | entities[i].idx = i; 159 | } 160 | 161 | for(var i = 0;i < links.length; i++){ 162 | if(_.isUndefined(entityToIdx[links[i].entity1_id]))continue; 163 | if(_.isUndefined(entityToIdx[links[i].entity2_id]))continue; 164 | 165 | var el = [entityToIdx[links[i].entity1_id], entityToIdx[links[i].entity2_id]]; 166 | 167 | if(_.isUndefined(linksByNode[el[0]])) 168 | linksByNode[el[0]] = []; 169 | linksByNode[el[0]].push(el[1]); 170 | 171 | if(_.isUndefined(linksByNode[el[1]])) 172 | linksByNode[el[1]] = []; 173 | linksByNode[el[1]].push(el[0]); 174 | } 175 | 176 | var k = _.keys(linksByNode); 177 | k.sort(); 178 | 179 | var topNodesLength = renderNodes.length; 180 | 181 | //Jeweils die Top 3 Knoten fuer jeden Knoten im Rendergraph hinzufuegen 182 | for(var i = 0;i < topNodesLength; i++){ 183 | //Alle benachbarten Knoten die noch nicht im Graph sind werden nach ihrem Pagerank sortiert und anschließend werden die Top 3 hinzugefuegt 184 | if(_.isEmpty(linksByNode[i]))continue; 185 | 186 | var remaining = []; 187 | _.each(linksByNode[i], function(neighbour){ 188 | if(!_.isUndefined(nodesInRenderGraph[neighbour]))return; 189 | 190 | remaining.push({ 191 | pageRank: entities[neighbour].pageRank, 192 | idx: neighbour 193 | }); 194 | }); 195 | 196 | if(_.isEmpty(remaining))continue; 197 | 198 | remaining.sort(function(a, b){ 199 | return b.pageRank - a.pageRank; 200 | }); 201 | 202 | for(var j = 0; j < Math.min(remaining.length, maxNeighbours); j++){ 203 | var idx = remaining[j].idx; 204 | var n = renderNodes.length; 205 | 206 | entities[idx].isTopNode = false; 207 | renderNodes.push(entities[idx]); 208 | nodesInRenderGraph[idx] = entities[idx]; 209 | } 210 | } 211 | 212 | /* 213 | Links by Node 214 | */ 215 | for(var i = 0;i < links.length; i++){ 216 | var el = [entityToIdx[links[i].entity1_id], entityToIdx[links[i].entity2_id]]; 217 | 218 | if(_.isUndefined(nodesInRenderGraph[el[0]])) 219 | continue; 220 | if(_.isUndefined(nodesInRenderGraph[el[1]])) 221 | continue; 222 | 223 | renderLinks.push(links[i]); 224 | linksById[links[i].id] = links[i]; 225 | } 226 | 227 | callback(); 228 | } 229 | ], function(err){ 230 | if(err){ 231 | res.sendStatus(500); 232 | console.log(err); 233 | return false; 234 | } 235 | 236 | res.send({ 237 | /*sites: sites, 238 | articles: articles,*/ 239 | nodes: nodesInRenderGraph, 240 | links: linksById 241 | }); 242 | }); 243 | }); 244 | } --------------------------------------------------------------------------------