summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAndré Glüpker <git@wgmd.de>2017-10-01 14:50:19 +0200
committerAndré Glüpker <git@wgmd.de>2017-10-01 14:50:19 +0200
commit1585395e7e530665c565b88fea15cbea68d3a88d (patch)
tree204033b68d01e27036a68d625f4474f3254880c2
downloadsteam-1585395e7e530665c565b88fea15cbea68d3a88d.tar.gz
steam-1585395e7e530665c565b88fea15cbea68d3a88d.tar.bz2
steam-1585395e7e530665c565b88fea15cbea68d3a88d.zip
Initial public release
-rw-r--r--.gitignore4
-rw-r--r--Caching.py41
-rwxr-xr-xDatabase.py34
-rw-r--r--QueryServer.py176
-rw-r--r--SteamAPI.py209
-rw-r--r--config/lobby.json.example30
-rw-r--r--config/secrets.json.example4
-rw-r--r--config/secrets.md7
-rwxr-xr-xmain.py173
-rw-r--r--static/hide.js17
-rw-r--r--static/style.css198
-rw-r--r--static/vivagraph.min.js2
-rw-r--r--templates/error.jinja16
-rw-r--r--templates/lobby_html.jinja100
-rw-r--r--templates/premades_html.jinja102
15 files changed, 1113 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..6265d09
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,4 @@
+cache
+docs
+__pycache__
+config/*.json
diff --git a/Caching.py b/Caching.py
new file mode 100644
index 0000000..a048af2
--- /dev/null
+++ b/Caching.py
@@ -0,0 +1,41 @@
+#!/usr/bin/env python3
+
+import os
+import time
+THIS_DIR = os.path.dirname(os.path.abspath(__file__))
+CACHE_DIR = os.path.join(THIS_DIR, 'cache')
+
+class Caching():
+
+ def readCache(profile, steamid, timetolife):
+ filepath = os.path.join(CACHE_DIR, profile)
+ filename = str(steamid)+'.tmp'
+ complete = os.path.join(filepath, filename)
+ try:
+ fileinfo = os.stat(complete)
+ except FileNotFoundError:
+ return False
+ if fileinfo.st_mtime < (time.time() - timetolife):
+ return False
+
+ with open(complete, 'rt') as cachefile:
+ content = cachefile.read()
+
+ return content
+
+ def writeCache(profile, steamid, content):
+ filepath = os.path.join(CACHE_DIR, profile)
+ filename = str(steamid)+'.tmp'
+ complete = os.path.join(filepath, filename)
+ try:
+ os.makedirs(filepath, exist_ok=True)
+ with open(complete, 'wt') as cachefile:
+ cachefile.write(content)
+ except:
+ print('Could not create cache file/dir.')
+ return False
+ return True
+
+if __name__ == "__main__":
+ # TODO(andre): Maybe run tests here?
+ print('This is a module.')
diff --git a/Database.py b/Database.py
new file mode 100755
index 0000000..001f2b2
--- /dev/null
+++ b/Database.py
@@ -0,0 +1,34 @@
+#!/usr/bin/env python3
+
+# import os
+# import time
+import sqlite3
+# database = sqlite3.connect('data.db')
+
+class Database():
+ def __init__(self):
+ self._connection = sqlite3.connect('data.db')
+ self._cursor = self._connection.cursor()
+ self._cursor.
+ # print(self._database)
+ print(self._cursor)
+
+ def initDB(self):
+ print('If db not exists...generate schema here')
+
+ def login(self, username, password):
+ print('try to login')
+ return 'userobject' or false
+ # Use flask session management?
+ # What crypt/hash to use?
+
+ def getMyTracked(self, userid):
+ return 'list of tracked users'
+
+ def msg(self, msg):
+ print(msg)
+
+if __name__ == "__main__":
+ database = Database()
+ database.initDB()
+ database.msg('2')
diff --git a/QueryServer.py b/QueryServer.py
new file mode 100644
index 0000000..669e7a3
--- /dev/null
+++ b/QueryServer.py
@@ -0,0 +1,176 @@
+#!/usr/bin/env python3
+
+# Time to live for caching. 0 to disable caching
+CACHE_TTL = 36000 #15
+
+sourcequery = [
+ 50, # HL Opposing Force
+ 240, # CS:S
+ 320, # HL2 DM
+ 440, # TF2
+ 550, # L4D2
+ 630, # AlienSwarm
+ 730, # CS:GO
+ 4000, # Garrys Mod
+ 107410, # ARMA III
+ # 221100, # DayZ
+ 252490, # Rust
+ 282440, # Quake Live
+ 328070, # Reflex
+ 346110 # ARK: Survival Evolved
+ ]
+sourcequeryinc = [
+ 107410, # ARMA III
+ 346110 # ARK: Survival Evolved
+ ]
+
+import json
+import socket
+from SteamAPI import SteamAPI
+if CACHE_TTL > 0:
+ from Caching import Caching
+
+class QueryServer():
+
+ def QueryServer(ip, port, gameid):
+ """Query a supported server.
+
+ Args:
+ ip: IP of the gameserver
+ port: Port of the gameserver
+ gameid: Steam gameid of running game
+ Returns:
+ dict() with parsed server values"""
+
+ port = int(port)
+ gameid = int(gameid)
+
+ if CACHE_TTL > 0:
+ cache = Caching.readCache('server', '%s-%s-%s' % (str(gameid), ip, str(port)), CACHE_TTL)
+
+ if cache:
+ print('From cache')
+ return json.loads(cache)
+
+ socket.setdefaulttimeout(5)
+ if int(gameid) in sourcequery:
+ print('Querying server...')
+ serverdata = QueryServer._querySourceServer(ip, port, gameid)
+
+ if CACHE_TTL > 0:
+ serialized = json.dumps(serverdata)
+ Caching.writeCache('server', '%s-%s-%s' % (str(gameid), ip, str(port)), serialized)
+
+ return serverdata
+
+ return None
+
+ def _querySourceServer(ip, port, gameid):
+ """Query servers using the source query protocol.
+ Some servers listen for these queries on an higher port."""
+ if gameid in sourcequeryinc:
+ port += 1
+
+ print(ip, port, gameid)
+ conn = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
+
+ try:
+ conn.connect( (ip, port) )
+ conn.sendall( b'\xFF\xFF\xFF\xFF\x54' + b'Source Engine Query' + b'\x00' )
+ message = conn.recv(4096)
+ except:
+ print('Exception in SourceQuery connection')
+ return None
+ finally:
+ conn.close()
+
+ data = dict()
+
+ # [0]-[3] -> 4*\xFF
+ data['header'] = chr(message[4])
+ data['protocol'] = chr(message[5])
+ message = message[6:]
+
+ # Servername
+ nullterm = message.index(b'\0')
+ data['name'] = message[:nullterm]
+ message = message[nullterm+1:]
+
+ # Mapname
+ nullterm = message.index(b'\0')
+ data['map'] = message[:nullterm]
+ message = message[nullterm+1:]
+
+ # Folder
+ nullterm = message.index(b'\0')
+ data['folder'] = message[:nullterm]
+ message = message[nullterm+1:]
+
+ # Game
+ nullterm = message.index(b'\0')
+ data['game'] = message[:nullterm]
+ message = message[nullterm+1:]
+
+ data['gameid'] = (message[1] << 8) + message[0]
+ data['players'] = message[2]
+ data['playersmax'] = message[3]
+ data['bots'] = message[4]
+ data['servertype'] = chr(message[5]) # dedicated/local/proxy(tv)
+ data['enviroment'] = chr(message[6]) # windows/linux/mac
+ data['visibility'] = message[7] # 0public 1private
+ data['vac'] = message[8]
+ message = message[9:]
+
+ # Game
+ nullterm = message.index(b'\0')
+ data['gameversion'] = message[:nullterm]
+ message = message[nullterm+1:]
+
+ extradataflag = message[0]
+ message = message[1:]
+
+ if extradataflag & 0x80:
+ data['port'] = (message[1] << 8) + message[0]
+ message = message[2:]
+
+ if extradataflag & 0x10:
+ data['steamid'] = (message[7] << 56) + (message[6] << 48) + \
+ (message[5] << 40) + (message[4] << 32) + \
+ (message[3] << 24) + (message[2] << 16) + \
+ (message[1] << 8) + (message[0])
+ message = message[8:]
+
+ if extradataflag & 0x40:
+ data['sourcetvport'] = (message[1] << 8) + message[0]
+ message = message[2:]
+ nullterm = message.index(b'\0')
+ data['sourcetvname'] = message[:nullterm]
+ message = message[nullterm+1:]
+
+ if extradataflag & 0x20:
+ nullterm = message.index(b'\0')
+ data['keywords'] = message[:nullterm]
+ message = message[nullterm+1:]
+
+ if extradataflag & 0x01:
+ data['sgameid'] = (message[7] << 56) + (message[6] << 48) + \
+ (message[5] << 40) + (message[4] << 32) + \
+ (message[3] << 24) + (message[2] << 16) + \
+ (message[1] << 8) + (message[0])
+ message = message[8:]
+
+ # Everything is type bytes, so convert things to utf8?
+ for key, value in data.items():
+ if isinstance(value, bytes):
+ try:
+ value = value.decode('utf-8', errors='replace')
+ data[key] = value
+ except UnicodeDecodeError:
+ data[key] = "UTF8 Error"
+ pass
+ return data
+
+if __name__ == "__main__":
+ # TODO(andre): Maybe run our tests here?
+ print('This is a module.')
+
diff --git a/SteamAPI.py b/SteamAPI.py
new file mode 100644
index 0000000..580b375
--- /dev/null
+++ b/SteamAPI.py
@@ -0,0 +1,209 @@
+#!/usr/bin/env python3
+
+from concurrent.futures import ThreadPoolExecutor
+from urllib.error import HTTPError
+from urllib.error import URLError
+from urllib.request import urlopen
+import json
+from Caching import Caching
+
+##################################################
+## Settings
+##################################################
+# Directory for a local cache & time to live for cache items in seconds
+CACHE = True
+
+##################################################
+
+class SteamAPI():
+ def __init__(self, token):
+ self.token = token
+
+ def getProfiles(self, steamids):
+ """Get steam profiles.
+
+ Args:
+ steamids: Steamids to fetch profiles for
+ Returns:
+ dict() with str(steamid) as keys
+ """
+ profile = dict()
+ steamids_copy = [str(steamid) for steamid in steamids]
+
+ if CACHE:
+ cachedids = []
+ for steamid in steamids_copy:
+ cache = Caching.readCache('profile', steamid, 15)
+ if cache:
+ cachedids.append(steamid)
+ profile[steamid] = json.loads(cache)
+ for steamid in cachedids:
+ steamids_copy.remove(steamid)
+
+ if len(steamids_copy):
+ steamidlist = ','.join([str(x) for x in steamids_copy])
+ url = 'https://api.steampowered.com/ISteamUser/GetPlayerSummaries/v0002/?format=json&key=%s&steamids=%s' % (self.token, steamidlist)
+ # print(url)
+ response = urlopen(url)
+ if response.status != 200:
+ print('Isn\'t this an HTTPError?')
+ return []
+ data = response.read().decode('utf-8')
+ jsondata = json.loads(data)
+
+ for player in jsondata['response']['players']:
+ currentid = player['steamid']
+ profile[currentid] = player
+
+ if CACHE:
+ # save newly loaded profiles
+ for steamid in steamids_copy:
+ Caching.writeCache('profile', steamid, json.dumps(profile[steamid]))
+
+ return profile
+
+ def getMultipleFriends(self, steamids):
+ executor = ThreadPoolExecutor(max_workers=10)
+ # Ask steam about friends for each
+ results = dict()
+ for steamid in steamids:
+ results[steamid] = executor.submit(self.getFriends, steamid)
+
+ profiles = executor.submit(self.getProfiles, steamids).result()
+
+ for steamid in steamids:
+ profiles[steamid]['friends'] = results[steamid].result()
+
+ return profiles
+
+ def getFriends(self, steamid):
+ """Fetch steam friends for given steamid.
+
+ Args:
+ steamid: Steamid, whose friendslist to fetch
+ Returns:
+ List of steamids. TODO(andre): We lose additional information here.
+ """
+ if CACHE:
+ cache = Caching.readCache('friends', steamid, 15*60)
+ if cache:
+ return json.loads(cache)
+
+ url = 'https://api.steampowered.com/ISteamUser/GetFriendList/v0001/?key=%s&steamid=%s&relationship=friend' % (self.token, str(steamid))
+ # print(url)
+ try:
+ response = urlopen(url)
+ data = response.read().decode('utf-8')
+ jsondata = json.loads(data)
+ except HTTPError:
+ # f.e. profile is private
+ jsondata = None
+
+ friends = []
+ if jsondata and 'friendslist' in jsondata and 'friends' in jsondata['friendslist']:
+ friends = [friend['steamid'] for friend in jsondata['friendslist']['friends']]
+ if CACHE:
+ Caching.writeCache('friends', steamid, json.dumps(friends))
+
+ return friends
+
+ def getGameSchema(self, gameid):
+ """Fetch info about a game.
+
+ Args:
+ gameid: Appid of the game
+ Returns:
+ gameName, gameVersion, availableGameStats (Achievements/Stats)
+ """
+ if CACHE:
+ cache = Caching.readCache('gameschema', gameid, 7*24*60*60)
+ if cache:
+ jsondata = json.loads(cache)
+ return jsondata['game']
+
+ url = 'http://api.steampowered.com/ISteamUserStats/GetSchemaForGame/v2/?key=%s&appid=%s&format=json' % (self.token, str(gameid))
+ print(url)
+ try:
+ response = urlopen(url)
+ data = response.read().decode('utf-8')
+ jsondata = json.loads(data)
+ except HTTPError as e:
+ # f.e. profile is private
+ print('Fetch failed.', e.code, e.reason)
+ jsondata = None
+
+ if 'game' in jsondata:
+ if CACHE:
+ Caching.writeCache('gameschema', gameid, json.dumps(jsondata))
+ return jsondata['game']
+ else:
+ print('No game in json:', jsondata)
+ return None
+
+ def getGames(self, steamid):
+ """Fetch a list of games a person possesses.
+
+ Args:
+ steamid: Steamid, whose gamelist to fetch
+ Returns:
+ Tuple with (number of games, gameinfo [appid, name, playtime_2weeks, playtime_forever, icons])
+ """
+ if CACHE:
+ cache = Caching.readCache('games', steamid, 60*60)
+ if cache:
+ jsondata = json.loads(cache)
+ return ( jsondata['response']['game_count'], jsondata['response']['games'] )
+
+ url = 'http://api.steampowered.com/IPlayerService/GetOwnedGames/v0001/?key=%s&steamid=%s&include_appinfo=1&format=json' % (self.token, str(steamid))
+ print(url)
+ try:
+ response = urlopen(url)
+ data = response.read().decode('utf-8')
+ jsondata = json.loads(data)
+ except HTTPError:
+ # f.e. profile is private
+ jsondata = None
+
+ if 'response' in jsondata and 'games' in jsondata['response']:
+ if CACHE:
+ Caching.writeCache('games', steamid, json.dumps(jsondata))
+ return ( jsondata['response']['game_count'], jsondata['response']['games'] )
+ return None
+
+ def getPlayerGameStats(self, steamid, gameid):
+ """Fetch the list of achievements a person achieved in a game
+
+ Args:
+ steamid: Steamid of the particular user
+ gameid: Appid of the game we want to check
+ Returns:
+ Tuple with (number of games, gameinfo [appid, name, playtime_2weeks, playtime_forever, icons])
+ """
+ if CACHE:
+ cache = Caching.readCache('playergamestats', '%s-%s' % (str(steamid), str(gameid)), 24*60*60)
+ if cache:
+ jsondata = json.loads(cache)
+ return jsondata
+ else:
+ print('nocache')
+
+ url = 'http://api.steampowered.com/ISteamUserStats/GetPlayerAchievements/v0001/?key=%s&steamid=%s&appid=%s' % (self.token, str(steamid), str(gameid))
+ print(url)
+ try:
+ response = urlopen(url)
+ data = response.read().decode('utf-8')
+ jsondata = json.loads(data)
+ except HTTPError:
+ # f.e. profile is private
+ jsondata = None
+
+ if 'playerstats' in jsondata:
+ if CACHE:
+ cache = Caching.writeCache('playergamestats', '%s-%s' % (str(steamid), str(gameid)), json.dumps(jsondata))
+ return jsondata
+ return None
+
+if __name__ == "__main__":
+ # TODO(andre): Maybe run tests here?
+ print('This is a module.')
+
diff --git a/config/lobby.json.example b/config/lobby.json.example
new file mode 100644
index 0000000..35891ce
--- /dev/null
+++ b/config/lobby.json.example
@@ -0,0 +1,30 @@
+{
+ "76561198002556232": { "main": "Cicco" },
+ "76561198196226474": { "main": "Cicco" },
+ "76561197980760765": { "main": "CoNo" },
+ "76561197961395108": { "main": "Crackhead" },
+ "76561198173319946": { "main": "Crackhead" },
+ "76561198134086527": { "main": "Crosby" },
+ "76561198236978570": { "main": "Crosby" },
+ "76561198057870737": { "main": "Dark Nightfall" },
+ "76561197960356051": { "main": "Der n00b" },
+ "76561197985005183": { "main": "MaD-.^" },
+ "76561198155724084": { "main": "MaD-.^" },
+ "76561198195069474": { "main": "MaD-.^" },
+ "76561198274742338": { "main": "MaD-.^" },
+ "76561198075447635": { "main": "Monte" },
+ "76561197963063991": { "main": "Penguin" },
+ "76561198155270977": { "main": "Penguin" },
+ "76561198157995991": { "main": "Penguin" },
+ "76561198091264830": { "main": "Pitchblack" },
+ "76561198118197952": { "main": "Quinto" },
+ "76561198213556884": { "main": "Quinto" },
+ "76561198041000035": { "main": "Seas" },
+ "76561197967103924": { "main": "Smoky" },
+ "76561197975243134": { "main": "Smoky" },
+ "76561198128110622": { "main": "Steak" },
+ "76561198036851146": { "main": "Stranger" },
+ "76561198161286427": { "main": "Stranger" },
+ "76561198213233440": { "main": "Stranger" },
+ "76561198215648187": { "main": "Stranger" }
+}
diff --git a/config/secrets.json.example b/config/secrets.json.example
new file mode 100644
index 0000000..726b04e
--- /dev/null
+++ b/config/secrets.json.example
@@ -0,0 +1,4 @@
+{
+ "flask_secret": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxxxxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxxxxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
+ "steam_token": "1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ"
+}
diff --git a/config/secrets.md b/config/secrets.md
new file mode 100644
index 0000000..51f548d
--- /dev/null
+++ b/config/secrets.md
@@ -0,0 +1,7 @@
+# Flask Secret
+Used throughout Flask for everything encryption related.
+Just put any random string here, this is the key for the encryptions.
+
+# Steam Token
+You need an API token, to query the steam API.
+Get your API token here: http://steamcommunity.com/dev/apikey
diff --git a/main.py b/main.py
new file mode 100755
index 0000000..d6c655a
--- /dev/null
+++ b/main.py
@@ -0,0 +1,173 @@
+#!/usr/bin/env python3
+
+from flask import Flask, flash, redirect, render_template, request, session, url_for
+
+# from wsgiref.util import setup_testing_defaults
+from collections import OrderedDict
+
+import json
+import os
+import re
+import sys
+import time, datetime
+# import traceback
+
+from SteamAPI import SteamAPI
+from QueryServer import QueryServer
+
+# Set some directories
+THIS_DIR = os.path.dirname(os.path.abspath(__file__))
+TEMPLATES = os.path.join(THIS_DIR, 'templates')
+ASSETS = os.path.join(THIS_DIR, 'assets')
+CONFIG = os.path.join(THIS_DIR, 'config')
+
+app = Flask(__name__)
+steam = None
+with open(os.path.join(CONFIG, 'secrets.json'), 'rt') as secretjson:
+ secrets = json.load(secretjson)
+ app.secret_key = secrets["flask_secret"]
+ steam = SteamAPI(secrets["steam_token"])
+
+##################################################
+## Helper functions
+##################################################
+
+intervals = (
+ ('w', 604800), # 60 * 60 * 24 * 7
+ ('d', 86400), # 60 * 60 * 24
+ ('h', 3600), # 60 * 60
+ ('m', 60),
+ ('s', 1),
+ )
+
+def display_time(seconds, granularity=2):
+ result = []
+ for name, count in intervals:
+ value = seconds // count
+ if value:
+ seconds -= value * count
+ if value == 1:
+ name = name.rstrip('s')
+ result.append("{}{}".format(int(value), name))
+ return ' '.join(result[:granularity])
+
+def grepSteamids(text):
+ steamids = []
+ SteamIDfromText = re.findall(r'STEAM_\d:(\d):(\d+)', text)
+ for steamid in SteamIDfromText:
+ steam64id = 76561197960265728 + int(steamid[0]) + (int(steamid[1]) * 2)
+ steamids.append(steam64id)
+ return steamids
+
+##################################################
+## Functions for different pages
+##################################################
+
+@app.route("/")
+def main():
+ return redirect(url_for('lobby'))
+
+@app.errorhandler(404)
+def not_found(e):
+ return render_template('error.jinja', error = 'Die angeforderte Seite konnte nicht gefunden werden.')
+
+@app.route("/lobby")
+def lobby():
+ steamids = dict()
+ friends = request.args.get('friends')
+
+ if friends:
+ for steamid in friends.split(','):
+ for friend in steam.getFriends(steamid):
+ if friend not in steamids:
+ steamids[friend] = {}
+ else:
+ # Load config (steamids, names)
+ with open(os.path.join(CONFIG, 'lobby.json'), 'rt') as config:
+ steamids = json.load(config)
+
+ profiledata = steam.getProfiles(steamids.keys())
+
+ # Merge new data in loaded config
+ for steamid in profiledata:
+ if steamid not in steamids:
+ continue
+ for key, value in profiledata[steamid].items():
+ steamids[steamid][key] = value
+
+ serverinfo = dict()
+ for steamid, playerdata in steamids.items():
+ if 'gameid' not in playerdata \
+ or 'gameserverip' not in playerdata:
+ continue
+ if ':' not in playerdata['gameserverip']:
+ continue
+ gameserver = playerdata['gameserverip']
+ if gameserver not in serverinfo:
+ ip, port = gameserver.split(':')
+ port = int(port)
+ gameid = playerdata['gameid']
+ print('Query Server:', ip, port, gameid)
+ server = QueryServer.QueryServer(ip, port, gameid)
+ print('Response:', server)
+ if server:
+ serverinfo[gameserver] = server
+
+ # Sort steamids to be more appealing
+ steamids = OrderedDict(sorted(steamids.items(), key = lambda player: player[1]['personaname'].lower()))
+ steamids = OrderedDict(sorted(steamids.items(), reverse=True, key = lambda player: int(player[1]['lastlogoff'])))
+ steamids = OrderedDict(sorted(steamids.items(), key = lambda player: (player[1]['personastate'] > 0) and player[1]['personastate'] or 10))
+ steamids = OrderedDict(sorted(steamids.items(), key = lambda player: ('gameextrainfo' in player[1] and player[1]['gameextrainfo'].lower() or "zzz")))
+
+ return render_template('lobby_html.jinja',
+ steamids = steamids,
+ serverinfo = serverinfo,
+ states = ['Offline', 'Online', 'Busy', 'Away', 'Snooze', 'Looking to trade', 'Looking to play'],
+ display_time = display_time,
+ current_time = time.time())
+
+@app.route("/premadefinder", methods=['GET', 'POST'])
+def premades():
+ steamids = []
+ profiles = dict()
+ connections = set()
+
+ if request.method == 'POST':
+ postdata = request.form['statustext']
+
+ steamids = grepSteamids(postdata)
+ steamids = [str(x) for x in steamids]
+ if len(steamids) > 50:
+ return render_template(
+ 'error.jinja',
+ error='Es sind maximal 50 Steamids erlaubt.'
+ )
+
+ # Ask steam about profiles
+ profiles = steam.getMultipleFriends(steamids)
+
+ # Add connection between friends.
+ # Friends are always bidirectional, so we use set and min/max to avoid duplicates
+ for steamid in steamids:
+ for friend in profiles[steamid]['friends']:
+ if friend in steamids:
+ friend_a = min(steamid, friend)
+ friend_b = max(steamid, friend)
+ connections.add((friend_a, friend_b))
+
+ return render_template('premades_html.jinja',
+ steamids=steamids,
+ profiles=profiles,
+ connections=connections
+ )
+
+if __name__ == '__main__':
+ # print(steam.getFriends("76561197963063991"))
+ # print(steam.getFriends("76561197963882989"))
+ app.run(threaded=True)
+
+# Changelog
+###########
+# Internals changed to use Flask framework
+# Some design changes
+# ?friends=steamid,steamid,...
diff --git a/static/hide.js b/static/hide.js
new file mode 100644
index 0000000..67cb5c9
--- /dev/null
+++ b/static/hide.js
@@ -0,0 +1,17 @@
+var hidden = false;
+
+function hideOffline() {
+ hidden = !hidden;
+ var playerboxes = document.querySelectorAll('div.Offline');
+ for(index in playerboxes) /* Show/Hide player row */
+ {
+ if(typeof playerboxes[index] != 'object') continue;
+ playerboxes[index].style.display = (hidden) ? 'none' : 'inline-block';
+ }
+ /* And update the text on our button. */
+ document.querySelector('#offlinetoggle').innerHTML = (hidden) ? 'Show Offline' : 'Hide Offline';
+}
+/* Execute function on page load. */
+setTimeout(hideOffline, 1);
+
+// vim: commentstring=/*\ %s\ */
diff --git a/static/style.css b/static/style.css
new file mode 100644
index 0000000..d206836
--- /dev/null
+++ b/static/style.css
@@ -0,0 +1,198 @@
+body {
+ color: white;
+ background-color: #1d1d1d;
+ font-family: sans-serif;
+}
+a {
+ color: white;
+ text-decoration: none;
+}
+
+ul#flash {
+ padding:10px;
+ margin:10px 0;
+ background-color: #FFBABA;
+ color: #D8000C;
+}
+
+ul#flash > li:before {
+ content: "\26A0";
+}
+
+ul#flash > li {
+ font-style:normal;
+ font-weight:400;
+ speak:none;
+ display:inline-block;
+ text-decoration:inherit;
+ margin-right:.2em;
+ text-align:center;
+ font-variant:normal;
+ text-transform:none;
+ line-height:1em;
+ margin-left:.2em;
+}
+
+ul#menu {
+ list-style-type: none;
+ margin-bottom: 20px;
+ padding: 0px;
+ overflow: hidden;
+ /* background-color: #ff00ff; */
+}
+
+li.menu {
+ float: left;
+ padding: 0px 5px;
+}
+
+li a {
+ display: block;
+ width: 150px;
+ text-align: center;
+ text-decoration: none;
+}
+
+li.app {
+ float: right;
+}
+
+li a:hover {
+ background-color: #ff0000;
+}
+
+.joinbutton {
+ color: white;
+ background-origin: padding-box;
+ background-image: linear-gradient(to bottom, rgb(164, 208, 7) 5%, rgb(83, 105, 4) 95%);
+ font-size: 12px;
+ line-height: 20px;
+ padding: 4px 6px 4px 6px;
+ border-radius: 4px;
+}
+
+.joinbutton:hover {
+ background-image: linear-gradient(to bottom, rgb(164, 208, 7) 25%, rgb(83, 105, 4) 75%);
+}
+
+/* Playerbox with Playerstats / Gamestats inside */
+div.player {
+ display: inline-block;
+ width: 330px;
+ min-height: 150px;
+ text-align: center;
+ margin: 2px 5px 2px 5px;
+ padding: 0px;
+ /* border: 1px solid black; */
+}
+/* Intern box of playerbox */
+div.interna {
+ display: inline-block;
+ vertical-align: top;
+ width: 120px;
+ height: 165px;
+ margin: 0px;
+ padding-bottom: 5px;
+ overflow: hidden;
+ /* background-color: pink; */
+}
+/* Additional settings for box with serverstats */
+div.stats {
+ font-size: small;
+ width: 200px;
+ /* background-color: purple; */
+}
+div.form {
+ border: 1px dashed gray;
+ text-align: center;
+}
+div#premades {
+ border: 1px dashed rgb(150, 150, 0);
+ height: 500px;
+}
+div#info {
+ text-align: center;
+}
+
+div.error {
+ border: 1px dashed red;
+ padding: 50px;
+ font-size: 150%;
+ text-align: center;
+}
+
+svg {
+ width: 100%;
+ height: 100%;
+}
+xmp {
+ margin: 5px;
+}
+
+/* Some special colors for 3rdpartygames, serverinfo, warnings etc */
+.error { color: rgb(200, 0, 0); font-weight: bold; }
+.info { color: rgb(200, 200, 0); font-size: smaller; font-weight: bold; }
+#disclaimer {
+ color: rgb(150, 150, 0);
+ font-size: smaller;
+ padding-left: 50px;
+ margin-top: 20px;
+ margin-bottom: 10px;
+}
+div.buttons { margin-top: 10px; }
+div.serverinfo { margin-top: 10px; margin-bottom: 10px; font-size: x-small; }
+img.avatar { width: 100px; height: 100px; margin: 1px 5px 1px 5px; }
+span.gamename { font-size: small; color: rgb(87, 203, 222); }
+span.ingameother { color: rgb(52, 132, 0); font-style: italic; }
+span.maininfo { font-size: x-small; }
+span.steamname { font-size: medium; /* font-family: serif; */ font-weight: bold; width: 110px; word-wrap: break-word; }
+img.gameimage { width: 95%; }
+
+/***************************************/
+/* Colors for different online states: */
+/* offline online busy away snooze */
+/* lookingtotrade lookingtoplay */
+/***************************************/
+/* Whole player info box */
+div.online { border: 1px solid rgb(87, 203, 222); }
+div.ingame { border: 1px solid rgb(144, 186, 60); }
+div.away { border: 1px solid rgb(255, 235, 0);
+ opacity: 0.6;
+ filter: alpha(opacity=60);
+}
+div.busy { border: 1px solid rgb(48, 134, 149);
+ opacity: 0.5;
+ filter: alpha(opacity=50);
+}
+div.snooze { border: 1px solid rgb(135, 134, 135);
+ opacity: 0.5;
+ filter: alpha(opacity=50);
+}
+div.lookingtotrade { border: 1px solid rgb(255, 255, 0); }
+div.lookingtoplay { border: 1px solid rgb(0, 255, 0); }
+div.offline {
+ border: 1px dashed rgb(137, 137, 137);
+ opacity: 0.4;
+ filter: alpha(opacity=40);
+}
+
+/* avatar */
+img.online { border: 1px solid rgb(87, 203, 222); }
+img.ingame { border: 1px solid rgb(144, 186, 60); }
+img.away { border: 1px solid rgb(255, 235, 0); }
+img.busy { border: 1px solid rgb(48, 134, 149); }
+img.snooze { border: 1px solid rgb(135, 134, 135); }
+img.lookingtotrade { border: 1px solid rgb(255, 255, 0); }
+img.lookingtoplay { border: 1px solid rgb(0, 255, 0); }
+img.offline { border: 1px dashed rgb(137, 137, 137); filter: grayscale(1); -webkit-filter: grayscale(1); }
+
+/* color for username / gameinfo */
+span.ingame,a.ingame { color: rgb(144, 186, 60); }
+span.online,a.online { color: rgb(87, 203, 222); }
+span.away,a.away { color: rgb(255, 235, 0); }
+span.busy { color: rgb(48, 134, 149); }
+span.snooze { color: rgb(135, 134, 135); }
+span.lookingtotrade { color: rgb(255, 255, 0); }
+span.lookingtoplay { color: rgb(0, 255, 0); }
+span.offline,a.offline { color: rgb(137, 137, 137); }
+
diff --git a/static/vivagraph.min.js b/static/vivagraph.min.js
new file mode 100644
index 0000000..c84b0b7
--- /dev/null
+++ b/static/vivagraph.min.js
@@ -0,0 +1,2 @@
+!function(e){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=e();else if("function"==typeof define&&define.amd)define([],e);else{var n;"undefined"!=typeof window?n=window:"undefined"!=typeof global?n=global:"undefined"!=typeof self&&(n=self),n.Viva=e()}}(function(){return function e(n,t,r){function o(a,u){if(!t[a]){if(!n[a]){var s="function"==typeof require&&require;if(!u&&s)return s(a,!0);if(i)return i(a,!0);var f=new Error("Cannot find module '"+a+"'");throw f.code="MODULE_NOT_FOUND",f}var c=t[a]={exports:{}};n[a][0].call(c.exports,function(e){var t=n[a][1][e];return o(t?t:e)},c,c.exports,e,n,t,r)}return t[a].exports}for(var i="function"==typeof require&&require,a=0;a<r.length;a++)o(r[a]);return o}({1:[function(e,n,t){var r=e("ngraph.random"),o={lazyExtend:function(){return e("ngraph.merge").apply(this,arguments)},randomIterator:function(){return r.randomIterator.apply(r,arguments)},random:function(){return r.random.apply(r,arguments)},events:e("ngraph.events")};o.Graph={version:e("./version.js"),graph:e("ngraph.graph"),serializer:function(){return{loadFromJSON:e("ngraph.fromjson"),storeToJSON:e("ngraph.tojson")}},centrality:e("./Algorithms/centrality.js"),operations:e("./Algorithms/operations.js"),geom:function(){return{intersect:e("gintersect"),intersectRect:e("./Utils/intersectRect.js")}},webgl:e("./WebGL/webgl.js"),webglInputEvents:e("./WebGL/webglInputEvents.js"),generator:function(){return e("ngraph.generators")},Input:{domInputManager:e("./Input/domInputManager.js"),webglInputManager:e("./Input/webglInputManager.js")},Utils:{dragndrop:e("./Input/dragndrop.js"),findElementPosition:e("./Utils/findElementPosition.js"),timer:e("./Utils/timer.js"),getDimension:e("./Utils/getDimensions.js"),events:e("./Utils/backwardCompatibleEvents.js")},Layout:{forceDirected:e("ngraph.forcelayout"),constant:e("./Layout/constant.js")},View:{Texture:e("./WebGL/texture.js"),webglAtlas:e("./WebGL/webglAtlas.js"),webglImageNodeProgram:e("./WebGL/webglImageNodeProgram.js"),webglLinkProgram:e("./WebGL/webglLinkProgram.js"),webglNodeProgram:e("./WebGL/webglNodeProgram.js"),webglLine:e("./WebGL/webglLine.js"),webglSquare:e("./WebGL/webglSquare.js"),webglImage:e("./WebGL/webglImage.js"),webglGraphics:e("./View/webglGraphics.js"),_webglUtil:{parseColor:e("./WebGL/parseColor.js")},svgGraphics:e("./View/svgGraphics.js"),renderer:e("./View/renderer.js"),cssGraphics:function(){throw new Error("cssGraphics is deprecated. Please use older version of vivagraph (< 0.7) if you need it")},svgNodeFactory:function(){throw new Error("svgNodeFactory is deprecated. Please use older version of vivagraph (< 0.7) if you need it")},community:function(){throw new Error("community is deprecated. Please use vivagraph < 0.7 if you need it, or `https://github.com/anvaka/ngraph.slpa` module")}},Rect:e("./Utils/rect.js"),svg:e("simplesvg"),BrowserInfo:e("./Utils/browserInfo.js")},n.exports=o},{"./Algorithms/centrality.js":32,"./Algorithms/operations.js":33,"./Input/domInputManager.js":34,"./Input/dragndrop.js":35,"./Input/webglInputManager.js":36,"./Layout/constant.js":37,"./Utils/backwardCompatibleEvents.js":38,"./Utils/browserInfo.js":39,"./Utils/findElementPosition.js":41,"./Utils/getDimensions.js":42,"./Utils/intersectRect.js":43,"./Utils/rect.js":45,"./Utils/timer.js":46,"./View/renderer.js":48,"./View/svgGraphics.js":49,"./View/webglGraphics.js":50,"./WebGL/parseColor.js":51,"./WebGL/texture.js":52,"./WebGL/webgl.js":53,"./WebGL/webglAtlas.js":54,"./WebGL/webglImage.js":55,"./WebGL/webglImageNodeProgram.js":56,"./WebGL/webglInputEvents.js":57,"./WebGL/webglLine.js":58,"./WebGL/webglLinkProgram.js":59,"./WebGL/webglNodeProgram.js":60,"./WebGL/webglSquare.js":61,"./version.js":62,gintersect:3,"ngraph.events":7,"ngraph.forcelayout":9,"ngraph.fromjson":10,"ngraph.generators":11,"ngraph.graph":12,"ngraph.merge":13,"ngraph.random":26,"ngraph.tojson":27,simplesvg:28}],2:[function(e,n,t){function r(e,n,t,r){return f=f||(document.addEventListener?{add:i,rm:a}:{add:u,rm:s}),f.add(e,n,t,r)}function o(e,n,t,r){return f=f||(document.addEventListener?{add:i,rm:a}:{add:u,rm:s}),f.rm(e,n,t,r)}function i(e,n,t,r){e.addEventListener(n,t,r)}function a(e,n,t,r){e.removeEventListener(n,t,r)}function u(e,n,t,r){if(r)throw new Error("cannot useCapture in oldIE");e.attachEvent("on"+n,t)}function s(e,n,t,r){e.detachEvent("on"+n,t)}r.removeEventListener=o,r.addEventListener=r,n.exports=r;var f=null},{}],3:[function(e,n,t){function r(e,n,t,r,o,i,a,u){var s,f,c,d,l,p,v,g,h,m,y,x,w,b={x:0,y:0};return s=r-n,c=e-t,l=t*n-e*r,h=s*o+c*i+l,m=s*a+c*u+l,0!==h&&0!==m&&h>=0==m>=4?null:(f=u-i,d=o-a,p=a*i-o*u,v=f*e+d*n+p,g=f*t+d*r+p,0!==v&&0!==g&&v>=0==g>=0?null:(y=s*d-f*c,0===y?null:(x=y<0?-y/2:y/2,x=0,w=c*p-d*l,b.x=(w<0?w-x:w+x)/y,w=f*l-s*p,b.y=(w<0?w-x:w+x)/y,b)))}n.exports=r},{}],4:[function(e,n,t){n.exports.degree=e("./src/degree.js"),n.exports.betweenness=e("./src/betweenness.js")},{"./src/betweenness.js":5,"./src/degree.js":6}],5:[function(e,n,t){function r(e,n){function t(e){g[e]/=2}function r(e){g[e.id]=0}function o(e){s=e.id,u(s),i()}function i(){for(e.forEachNode(a);c.length;){for(var n=c.pop(),t=(1+v[n])/p[n],r=d[n],o=0;o<r.length;++o){var i=r[o];v[i]+=p[i]*t}n!==s&&(g[n]+=v[n])}}function a(e){v[e.id]=0}function u(t){function r(e){i(e.id)}function o(e){var n=e.id;d[n]=[],l[n]=-1,p[n]=0}function i(e){l[e]===-1&&(l[e]=l[a]+1,f.push(e)),l[e]===l[a]+1&&(p[e]+=p[a],d[e].push(a))}for(e.forEachNode(o),l[t]=0,p[t]=1,f.push(t);f.length;){var a=f.shift();Object.create(null);c.push(a),e.forEachLinkedNode(a,r,n)}}var s,f=[],c=[],d=Object.create(null),l=Object.create(null),p=Object.create(null),v=Object.create(null),g=Object.create(null);return e.forEachNode(r),e.forEachNode(o),n||Object.keys(g).forEach(t),g}n.exports=r},{}],6:[function(e,n,t){function r(e,n){function t(n){var t=e.getLinks(n.id);u[n.id]=r(t,n.id)}var r,u=Object.create(null);if(n=(n||"both").toLowerCase(),"both"===n||"inout"===n)r=a;else if("in"===n)r=o;else{if("out"!==n)throw new Error("Expected centrality degree kind is: in, out or both");r=i}return e.forEachNode(t),u}function o(e,n){for(var t=0,r=0;r<e.length;r+=1)t+=e[r].toId===n?1:0;return t}function i(e,n){for(var t=0,r=0;r<e.length;r+=1)t+=e[r].fromId===n?1:0;return t}function a(e){return e.length}n.exports=r},{}],7:[function(e,n,t){function r(e){var n=Object.create(null);return{on:function(t,r,o){if("function"!=typeof r)throw new Error("callback is expected to be a function");var i=n[t];return i||(i=n[t]=[]),i.push({callback:r,ctx:o}),e},off:function(t,r){var o="undefined"==typeof t;if(o)return n=Object.create(null),e;if(n[t]){var i="function"!=typeof r;if(i)delete n[t];else for(var a=n[t],u=0;u<a.length;++u)a[u].callback===r&&a.splice(u,1)}return e},fire:function(t){var r=n[t];if(!r)return e;var o;arguments.length>1&&(o=Array.prototype.splice.call(arguments,1));for(var i=0;i<r.length;++i){var a=r[i];a.callback.apply(a.ctx,o)}return e}}}function o(e){if(!e)throw new Error("Eventify cannot use falsy object as events subject");for(var n=["on","fire","off"],t=0;t<n.length;++t)if(e.hasOwnProperty(n[t]))throw new Error("Subject cannot be eventified, since it already has property '"+n[t]+"'")}n.exports=function(e){o(e);var n=r(e);return e.on=n.on,e.off=n.off,e.fire=n.fire,e}},{}],8:[function(e,n,t){function r(e,n,t){var r="[object Array]"===Object.prototype.toString.call(t);if(r)for(var i=0;i<t.length;++i)o(e,n,t[i]);else for(var a in e)o(e,n,a)}function o(e,n,t){if(e.hasOwnProperty(t)){if("function"==typeof n[t])return;n[t]=function(r){return void 0!==r?(e[t]=r,n):e[t]}}}n.exports=r},{}],9:[function(e,n,t){function r(n,t){function r(e){Object.keys(L).forEach(function(n){e(L[n],n)})}function a(e,t){var r;if(void 0===t)r="object"!=typeof e?e:e.id;else{var o=n.hasLink(e,t);if(!o)return;r=o.id}return N[r]}function u(e){return L[e]}function s(){n.on("changed",c)}function f(e){A.fire("stable",e)}function c(e){for(var t=0;t<e.length;++t){var r=e[t];"add"===r.changeType?(r.node&&l(r.node.id),r.link&&v(r.link)):"remove"===r.changeType&&(r.node&&p(r.node),r.link&&g(r.link))}P=n.getNodesCount()}function d(){P=0,n.forEachNode(function(e){l(e.id),P+=1}),n.forEachLink(v)}function l(e){var t=L[e];if(!t){var r=n.getNode(e);if(!r)throw new Error("initBody() was called with unknown node id");var o=r.position;if(!o){var i=h(r);o=E.getBestNewBodyPosition(i)}t=E.addBodyAt(o),t.id=e,L[e]=t,m(e),y(r)&&(t.isPinned=!0)}}function p(e){var n=e.id,t=L[n];t&&(L[n]=null,delete L[n],E.removeBody(t))}function v(e){m(e.fromId),m(e.toId);var n=L[e.fromId],t=L[e.toId],r=E.addSpring(n,t,e.length);k(e,r),N[e.id]=r}function g(e){var t=N[e.id];if(t){var r=n.getNode(e.fromId),o=n.getNode(e.toId);r&&m(r.id),o&&m(o.id),delete N[e.id],E.removeSpring(t)}}function h(e){var n=[];if(!e.links)return n;for(var t=Math.min(e.links.length,2),r=0;r<t;++r){var o=e.links[r],i=o.fromId!==e.id?L[o.fromId]:L[o.toId];i&&i.pos&&n.push(i)}return n}function m(e){var n=L[e];n.mass=w(e)}function y(e){return e&&(e.isPinned||e.data&&e.data.isPinned)}function x(e){var n=L[e];return n||(l(e),n=L[e]),n}function w(e){var t=n.getLinks(e);return t?1+t.length/3:1}if(!n)throw new Error("Graph structure cannot be undefined");var b=e("ngraph.physics.simulator"),E=b(t),L=Object.create(null),N={},P=0,k=E.settings.springTransform||o;d(),s();var j=!1,A={step:function(){if(0===P)return!0;var e=E.step();A.lastMove=e,A.fire("step");var n=e/P,t=n<=.01;return j!==t&&(j=t,f(t)),t},getNodePosition:function(e){return x(e).pos},setNodePosition:function(e){var n=x(e);n.setPosition.apply(n,Array.prototype.slice.call(arguments,1))},getLinkPosition:function(e){var n=N[e];if(n)return{from:n.from.pos,to:n.to.pos}},getGraphRect:function(){return E.getBBox()},forEachBody:r,pinNode:function(e,n){var t=x(e.id);t.isPinned=!!n},isNodePinned:function(e){return x(e.id).isPinned},dispose:function(){n.off("changed",c),A.fire("disposed")},getBody:u,getSpring:a,simulator:E,graph:n,lastMove:0};return i(A),A}function o(){}n.exports=r,n.exports.simulator=e("ngraph.physics.simulator");var i=e("ngraph.events")},{"ngraph.events":7,"ngraph.physics.simulator":15}],10:[function(e,n,t){function r(e,n,t){var r;n=n||o,t=t||o,r="string"==typeof e?JSON.parse(e):e;var a,u=i();if(void 0===r.links||void 0===r.nodes)throw new Error("Cannot load graph without links and nodes");for(a=0;a<r.nodes.length;++a){var s=n(r.nodes[a]);if(!s.hasOwnProperty("id"))throw new Error("Graph node format is invalid: Node id is missing");u.addNode(s.id,s.data)}for(a=0;a<r.links.length;++a){var f=t(r.links[a]);if(!f.hasOwnProperty("fromId")||!f.hasOwnProperty("toId"))throw new Error("Graph link format is invalid. Both fromId and toId are required");u.addLink(f.fromId,f.toId,f.data)}return u}function o(e){return e}n.exports=r;var i=e("ngraph.graph")},{"ngraph.graph":12}],11:[function(e,n,t){function r(e){if(!e||e<0)throw new Error("Invalid number of nodes");var n,t=p();for(n=0;n<e-1;++n)t.addLink(n,n+1),t.addLink(e+n,e+n+1),t.addLink(n,e+n);return t.addLink(e-1,2*e-1),t}function o(e){if(!e||e<0)throw new Error("Invalid number of nodes");var n=r(e);return n.addLink(0,e-1),n.addLink(e,2*e-1),n}function i(e){if(!e||e<1)throw new Error("At least two nodes are expected for complete graph");var n,t,r=p();for(n=0;n<e;++n)for(t=n+1;t<e;++t)n!==t&&r.addLink(n,t);return r}function a(e,n){if(!e||!n||e<0||n<0)throw new Error("Graph dimensions are invalid. Number of nodes in each partition should be greater than 0");var t,r,o=p();for(t=0;t<e;++t)for(r=e;r<e+n;++r)o.addLink(t,r);return o}function u(e){if(!e||e<0)throw new Error("Invalid number of nodes");var n,t=p();for(t.addNode(0),n=1;n<e;++n)t.addLink(n-1,n);return t}function s(e,n){if(e<1||n<1)throw new Error("Invalid number of nodes in grid graph");var t,r,o=p();if(1===e&&1===n)return o.addNode(0),o;for(t=0;t<e;++t)for(r=0;r<n;++r){var i=t+r*e;t>0&&o.addLink(i,t-1+r*e),r>0&&o.addLink(i,t+(r-1)*e)}return o}function f(e,n,t){if(e<1||n<1||t<1)throw new Error("Invalid number of nodes in grid3 graph");var r,o,i,a=p();if(1===e&&1===n&&1===t)return a.addNode(0),a;for(i=0;i<t;++i)for(r=0;r<e;++r)for(o=0;o<n;++o){var u=i*e*n,s=r+o*e+u;r>0&&a.addLink(s,r-1+o*e+u),o>0&&a.addLink(s,r+(o-1)*e+u),i>0&&a.addLink(s,r+o*e+(i-1)*e*n)}return a}function c(e){if(e<0)throw new Error("Invalid number of nodes in balanced tree");var n,t=p(),r=Math.pow(2,e);for(0===e&&t.addNode(1),n=1;n<r;++n){var o=n,i=2*o,a=2*o+1;t.addLink(o,i),t.addLink(o,a)}return t}function d(e){if(e<0)throw new Error("Number of nodes shoul be >= 0");var n,t=p();for(n=0;n<e;++n)t.addNode(n);return t}function l(n,t,r,o){if(t>=n)throw new Error("Choose smaller `k`. It cannot be larger than number of nodes `n`");var i,a,u=e("ngraph.random").random(o||42),s=p();for(i=0;i<n;++i)s.addNode(i);for(var f=Math.floor(t/2+1),c=1;c<f;++c)for(i=0;i<n;++i)a=(c+i)%n,s.addLink(i,a);for(c=1;c<f;++c)for(i=0;i<n;++i)if(u.nextDouble()<r){var d=i;a=(c+i)%n;var l=u.next(n),v=l===d||s.hasLink(d,l);if(v&&s.getLinks(d).length===n-1)continue;for(;v;)l=u.next(n),v=l===d||s.hasLink(d,l);var g=s.hasLink(d,a);s.removeLink(g),s.addLink(d,l)}return s}n.exports={ladder:r,complete:i,completeBipartite:a,balancedBinTree:c,path:u,circularLadder:o,grid:s,grid3:f,noLinks:d,wattsStrogatz:l};var p=e("ngraph.graph")},{"ngraph.graph":12,"ngraph.random":26}],12:[function(e,n,t){function r(e){function n(){function e(){return O.beginUpdate=B=L,O.endUpdate=F=N,U=t,R=r,O.on=n,n.apply(O,arguments)}var n=O.on;O.on=e}function t(e,n){M.push({link:e,changeType:n})}function r(e,n){M.push({node:e,changeType:n})}function s(e,n){if(void 0===e)throw new Error("Invalid node identifier");B();var t=f(e);return t?R(t,"update"):(t=new i(e),T++,R(t,"add")),t.data=n,A[e]=t,F(),t}function f(e){return A[e]}function c(e){var n=f(e);if(!n)return!1;for(B();n.links.length;){var t=n.links[0];g(t)}return delete A[e],T--,R(n,"remove"),F(),!0}function d(e,n,t){B();var r=f(e)||s(e),o=f(n)||s(n),i=D(e,n,t);return _.push(i),r.links.push(i),e!==n&&o.links.push(i),U(i,"add"),F(),i}function l(e,n,t){var r=e.toString()+n.toString();return new a(e,n,t,r)}function p(e,n,t){var r=e.toString()+"👉 "+n.toString(),o=I.hasOwnProperty(r);return(o||h(e,n))&&(o||(I[r]=0),r+="@"+ ++I[r]),new a(e,n,t,r)}function v(e){var n=f(e);return n?n.links:null}function g(e){if(!e)return!1;var n=o(e,_);if(n<0)return!1;B(),_.splice(n,1);var t=f(e.fromId),r=f(e.toId);return t&&(n=o(e,t.links),n>=0&&t.links.splice(n,1)),r&&(n=o(e,r.links),n>=0&&r.links.splice(n,1)),U(e,"remove"),F(),!0}function h(e,n){var t,r=f(e);if(!r)return null;for(t=0;t<r.links.length;++t){var o=r.links[t];if(o.fromId===e&&o.toId===n)return o}return null}function m(){B(),S(function(e){c(e.id)}),F()}function y(e){var n,t;if("function"==typeof e)for(n=0,t=_.length;n<t;++n)e(_[n])}function x(e,n,t){var r=f(e);if(r&&r.links&&"function"==typeof n)return t?b(r.links,e,n):w(r.links,e,n)}function w(e,n,t){for(var r,o=0;o<e.length;++o){var i=e[o],a=i.fromId===n?i.toId:i.fromId;if(r=t(A[a],i))return!0}}function b(e,n,t){for(var r,o=0;o<e.length;++o){var i=e[o];if(i.fromId===n&&(r=t(A[i.toId],i)))return!0}}function E(){}function L(){C+=1}function N(){C-=1,0===C&&M.length>0&&(O.fire("changed",M),M.length=0)}function P(){return Object.keys?k:j}function k(e){if("function"==typeof e)for(var n=Object.keys(A),t=0;t<n.length;++t)if(e(A[n[t]]))return!0}function j(e){if("function"==typeof e){var n;for(n in A)if(e(A[n]))return!0}}e=e||{},void 0===e.uniqueLinkId&&(e.uniqueLinkId=!0);var A="function"==typeof Object.create?Object.create(null):{},_=[],I={},T=0,C=0,S=P(),D=e.uniqueLinkId?p:l,M=[],U=E,R=E,B=E,F=E,O={addNode:s,addLink:d,removeLink:g,removeNode:c,getNode:f,getNodesCount:function(){return T},getLinksCount:function(){return _.length},getLinks:v,forEachNode:S,forEachLinkedNode:x,forEachLink:y,beginUpdate:B,endUpdate:F,clear:m,hasLink:h,getLink:h};return u(O),n(),O}function o(e,n){if(n.indexOf)return n.indexOf(e);var t,r=n.length;for(t=0;t<r;t+=1)if(n[t]===e)return t;return-1}function i(e){this.id=e,this.links=[],this.data=null}function a(e,n,t,r){this.fromId=e,this.toId=n,this.data=t,this.id=r}n.exports=r;var u=e("ngraph.events")},{"ngraph.events":7}],13:[function(e,n,t){function r(e,n){var t;if(e||(e={}),n)for(t in n)if(n.hasOwnProperty(t)){var o=e.hasOwnProperty(t),i=typeof n[t],a=!o||typeof e[t]!==i;a?e[t]=n[t]:"object"===i&&(e[t]=r(e[t],n[t]))}return e}n.exports=r},{}],14:[function(e,n,t){function r(e,n){this.pos=new o(e,n),this.prevPos=new o(e,n),this.force=new o,this.velocity=new o,this.mass=1}function o(e,n){e&&"number"!=typeof e?(this.x="number"==typeof e.x?e.x:0,this.y="number"==typeof e.y?e.y:0):(this.x="number"==typeof e?e:0,this.y="number"==typeof n?n:0)}function i(e,n,t){this.pos=new a(e,n,t),this.prevPos=new a(e,n,t),this.force=new a,this.velocity=new a,this.mass=1}function a(e,n,t){e&&"number"!=typeof e?(this.x="number"==typeof e.x?e.x:0,this.y="number"==typeof e.y?e.y:0,this.z="number"==typeof e.z?e.z:0):(this.x="number"==typeof e?e:0,this.y="number"==typeof n?n:0,this.z="number"==typeof t?t:0)}n.exports={Body:r,Vector2d:o,Body3d:i,Vector3d:a},r.prototype.setPosition=function(e,n){this.prevPos.x=this.pos.x=e,this.prevPos.y=this.pos.y=n},o.prototype.reset=function(){this.x=this.y=0},i.prototype.setPosition=function(e,n,t){this.prevPos.x=this.pos.x=e,this.prevPos.y=this.pos.y=n,this.prevPos.z=this.pos.z=t},a.prototype.reset=function(){this.x=this.y=this.z=0}},{}],15:[function(e,n,t){function r(n){function t(){var e,n=p.length;if(n)for(g.insertBodies(p);n--;)e=p[n],e.isPinned||(e.force.reset(),g.updateBodyForce(e),y.update(e));for(n=v.length;n--;)m.update(v[n])}var r=e("./lib/spring"),o=e("ngraph.expose"),i=e("ngraph.merge"),a=e("ngraph.events");n=i(n,{springLength:30,springCoeff:8e-4,gravity:-1.2,theta:.8,dragCoeff:.02,timeStep:20});var u=n.createQuadTree||e("ngraph.quadtreebh"),s=n.createBounds||e("./lib/bounds"),f=n.createDragForce||e("./lib/dragForce"),c=n.createSpringForce||e("./lib/springForce"),d=n.integrator||e("./lib/eulerIntegrator"),l=n.createBody||e("./lib/createBody"),p=[],v=[],g=u(n),h=s(p,n),m=c(n),y=f(n),x=0,w={bodies:p,quadTree:g,springs:v,settings:n,step:function(){t();var e=d(p,n.timeStep);return h.update(),e},addBody:function(e){if(!e)throw new Error("Body is required");return p.push(e),e},addBodyAt:function(e){if(!e)throw new Error("Body position is required");var n=l(e);return p.push(n),n},removeBody:function(e){if(e){var n=p.indexOf(e);if(!(n<0))return p.splice(n,1),0===p.length&&h.reset(),!0}},addSpring:function(e,n,t,o,i){if(!e||!n)throw new Error("Cannot add null spring to force simulator");"number"!=typeof t&&(t=-1);var a=new r(e,n,t,i>=0?i:-1,o);return v.push(a),a},getTotalMovement:function(){return x},removeSpring:function(e){if(e){var n=v.indexOf(e);return n>-1?(v.splice(n,1),!0):void 0}},getBestNewBodyPosition:function(e){return h.getBestNewPosition(e)},getBBox:function(){return h.box},gravity:function(e){return void 0!==e?(n.gravity=e,g.options({gravity:e}),this):n.gravity},theta:function(e){return void 0!==e?(n.theta=e,g.options({theta:e}),this):n.theta}};return o(n,w),a(w),w}n.exports=r},{"./lib/bounds":16,"./lib/createBody":17,"./lib/dragForce":18,"./lib/eulerIntegrator":19,"./lib/spring":20,"./lib/springForce":21,"ngraph.events":7,"ngraph.expose":8,"ngraph.merge":13,"ngraph.quadtreebh":22}],16:[function(e,n,t){n.exports=function(n,t){function r(){var e=n.length;if(0!==e){for(var t=Number.MAX_VALUE,r=Number.MAX_VALUE,o=Number.MIN_VALUE,a=Number.MIN_VALUE;e--;){var u=n[e];u.isPinned?(u.pos.x=u.prevPos.x,u.pos.y=u.prevPos.y):(u.prevPos.x=u.pos.x,u.prevPos.y=u.pos.y),u.pos.x<t&&(t=u.pos.x),u.pos.x>o&&(o=u.pos.x),u.pos.y<r&&(r=u.pos.y),u.pos.y>a&&(a=u.pos.y)}i.x1=t,i.x2=o,i.y1=r,i.y2=a}}var o=e("ngraph.random").random(42),i={x1:0,y1:0,x2:0,y2:0};return{box:i,update:r,reset:function(){i.x1=i.y1=0,i.x2=i.y2=0},getBestNewPosition:function(e){var n=i,r=0,a=0;if(e.length){for(var u=0;u<e.length;++u)r+=e[u].pos.x,a+=e[u].pos.y;r/=e.length,a/=e.length}else r=(n.x1+n.x2)/2,a=(n.y1+n.y2)/2;var s=t.springLength;return{x:r+o.next(s)-s/2,y:a+o.next(s)-s/2}}}}},{"ngraph.random":26}],17:[function(e,n,t){var r=e("ngraph.physics.primitives");n.exports=function(e){return new r.Body(e)}},{"ngraph.physics.primitives":14}],18:[function(e,n,t){n.exports=function(n){var t=e("ngraph.merge"),r=e("ngraph.expose");n=t(n,{dragCoeff:.02});var o={update:function(e){e.force.x-=n.dragCoeff*e.velocity.x,e.force.y-=n.dragCoeff*e.velocity.y}};return r(n,o,["dragCoeff"]),o}},{"ngraph.expose":8,"ngraph.merge":13}],19:[function(e,n,t){function r(e,n){var t,r=0,o=0,i=0,a=0,u=e.length;if(0===u)return 0;for(t=0;t<u;++t){var s=e[t],f=n/s.mass;s.velocity.x+=f*s.force.x,s.velocity.y+=f*s.force.y;var c=s.velocity.x,d=s.velocity.y,l=Math.sqrt(c*c+d*d);l>1&&(s.velocity.x=c/l,s.velocity.y=d/l),r=n*s.velocity.x,i=n*s.velocity.y,s.pos.x+=r,s.pos.y+=i,o+=Math.abs(r),a+=Math.abs(i)}return(o*o+a*a)/u}n.exports=r},{}],20:[function(e,n,t){function r(e,n,t,r,o){this.from=e,this.to=n,this.length=t,this.coeff=r,this.weight="number"==typeof o?o:1}n.exports=r},{}],21:[function(e,n,t){n.exports=function(n){var t=e("ngraph.merge"),r=e("ngraph.random").random(42),o=e("ngraph.expose");n=t(n,{springCoeff:2e-4,springLength:80});var i={update:function(e){var t=e.from,o=e.to,i=e.length<0?n.springLength:e.length,a=o.pos.x-t.pos.x,u=o.pos.y-t.pos.y,s=Math.sqrt(a*a+u*u);0===s&&(a=(r.nextDouble()-.5)/50,u=(r.nextDouble()-.5)/50,s=Math.sqrt(a*a+u*u));var f=s-i,c=(!e.coeff||e.coeff<0?n.springCoeff:e.coeff)*f/s*e.weight;t.force.x+=c*a,t.force.y+=c*u,o.force.x-=c*a,o.force.y-=c*u}};return o(n,i,["springCoeff","springLength"]),i}},{"ngraph.expose":8,"ngraph.merge":13,"ngraph.random":26}],22:[function(e,n,t){function r(e,n){return 0===n?e.quad0:1===n?e.quad1:2===n?e.quad2:3===n?e.quad3:null}function o(e,n,t){0===n?e.quad0=t:1===n?e.quad1=t:2===n?e.quad2=t:3===n&&(e.quad3=t)}n.exports=function(n){function t(){var e=h[m];return e?(e.quad0=null,e.quad1=null,e.quad2=null,e.quad3=null,e.body=null,e.mass=e.massX=e.massY=0,e.left=e.right=e.top=e.bottom=0):(e=new f,h[m]=e),++m,e}function i(e){var n,t,r,o,i=p,a=0,u=0,f=1,c=0,d=1;for(i[0]=y;f;){var v=i[c],h=v.body;f-=1,c+=1;var m=h!==e;h&&m?(t=h.pos.x-e.pos.x,r=h.pos.y-e.pos.y,o=Math.sqrt(t*t+r*r),0===o&&(t=(s.nextDouble()-.5)/50,r=(s.nextDouble()-.5)/50,o=Math.sqrt(t*t+r*r)),n=l*h.mass*e.mass/(o*o*o),a+=n*t,u+=n*r):m&&(t=v.massX/v.mass-e.pos.x,r=v.massY/v.mass-e.pos.y,o=Math.sqrt(t*t+r*r),0===o&&(t=(s.nextDouble()-.5)/50,r=(s.nextDouble()-.5)/50,o=Math.sqrt(t*t+r*r)),(v.right-v.left)/o<g?(n=l*v.mass*e.mass/(o*o*o),a+=n*t,u+=n*r):(v.quad0&&(i[d]=v.quad0,f+=1,d+=1),v.quad1&&(i[d]=v.quad1,f+=1,d+=1),v.quad2&&(i[d]=v.quad2,f+=1,d+=1),v.quad3&&(i[d]=v.quad3,f+=1,d+=1)))}e.force.x+=a,e.force.y+=u}function a(e){var n,r=Number.MAX_VALUE,o=Number.MAX_VALUE,i=Number.MIN_VALUE,a=Number.MIN_VALUE,s=e.length;for(n=s;n--;){var f=e[n].pos.x,c=e[n].pos.y;f<r&&(r=f),f>i&&(i=f),c<o&&(o=c),c>a&&(a=c)}var d=i-r,l=a-o;for(d>l?a=o+d:i=r+l,m=0,y=t(),y.left=r,y.right=i,y.top=o,y.bottom=a,n=s-1,n>=0&&(y.body=e[n]);n--;)u(e[n],y)}function u(e){for(v.reset(),v.push(y,e);!v.isEmpty();){var n=v.pop(),i=n.node,a=n.body;if(i.body){var u=i.body;if(i.body=null,d(u.pos,a.pos)){var f=3;do{var c=s.nextDouble(),l=(i.right-i.left)*c,p=(i.bottom-i.top)*c;u.pos.x=i.left+l,u.pos.y=i.top+p,f-=1}while(f>0&&d(u.pos,a.pos));if(0===f&&d(u.pos,a.pos))return}v.push(i,u),v.push(i,a)}else{var g=a.pos.x,h=a.pos.y;i.mass=i.mass+a.mass,i.massX=i.massX+a.mass*g,i.massY=i.massY+a.mass*h;var m=0,x=i.left,w=(i.right+x)/2,b=i.top,E=(i.bottom+b)/2;g>w&&(m+=1,x=w,w=i.right),h>E&&(m+=2,b=E,E=i.bottom);var L=r(i,m);L?v.push(L,a):(L=t(),L.left=x,L.top=b,L.right=w,L.bottom=E,L.body=a,o(i,m,L))}}}n=n||{},n.gravity="number"==typeof n.gravity?n.gravity:-1,n.theta="number"==typeof n.theta?n.theta:.8;var s=e("ngraph.random").random(1984),f=e("./node"),c=e("./insertStack"),d=e("./isSamePosition"),l=n.gravity,p=[],v=new c,g=n.theta,h=[],m=0,y=t();return{insertBodies:a,getRoot:function(){return y},updateBodyForce:i,options:function(e){return e?("number"==typeof e.gravity&&(l=e.gravity),"number"==typeof e.theta&&(g=e.theta),this):{gravity:l,theta:g}}}}},{"./insertStack":23,"./isSamePosition":24,"./node":25,"ngraph.random":26}],23:[function(e,n,t){function r(){this.stack=[],this.popIdx=0}function o(e,n){this.node=e,this.body=n}n.exports=r,r.prototype={isEmpty:function(){return 0===this.popIdx},push:function(e,n){var t=this.stack[this.popIdx];t?(t.node=e,t.body=n):this.stack[this.popIdx]=new o(e,n),++this.popIdx},pop:function(){if(this.popIdx>0)return this.stack[--this.popIdx]},reset:function(){this.popIdx=0}}},{}],24:[function(e,n,t){n.exports=function(e,n){var t=Math.abs(e.x-n.x),r=Math.abs(e.y-n.y);return t<1e-8&&r<1e-8}},{}],25:[function(e,n,t){n.exports=function(){this.body=null,this.quad0=null,this.quad1=null,this.quad2=null,this.quad3=null,this.mass=0,this.massX=0,this.massY=0,this.left=0,this.top=0,this.bottom=0,this.right=0}},{}],26:[function(e,n,t){function r(e){var n="number"==typeof e?e:+new Date,t=function(){return n=n+2127912214+(n<<12)&4294967295,n=4294967295&(3345072700^n^n>>>19),n=n+374761393+(n<<5)&4294967295,n=4294967295&(n+3550635116^n<<9),n=n+4251993797+(n<<3)&4294967295,n=4294967295&(3042594569^n^n>>>16),(268435455&n)/268435456};return{next:function(e){return Math.floor(t()*e)},nextDouble:function(){return t()}}}function o(e,n){var t=n||r();if("function"!=typeof t.next)throw new Error("customRandom does not match expected API: next() function is missing");return{forEach:function(n){var r,o,i;for(r=e.length-1;r>0;--r)o=t.next(r+1),i=e[o],e[o]=e[r],e[r]=i,n(i);e.length&&n(e[0])},shuffle:function(){var n,r,o;for(n=e.length-1;n>0;--n)r=t.next(n+1),o=e[r],e[r]=e[n],e[n]=o;return e}}}n.exports={random:r,randomIterator:o}},{}],27:[function(e,n,t){function r(e,n,t){function r(e){u.nodes.push(s(e))}function o(e){u.links.push(f(e))}function i(e){var n={id:e.id};return void 0!==e.data&&(n.data=e.data),n}function a(e){var n={fromId:e.fromId,toId:e.toId};return void 0!==e.data&&(n.data=e.data),n}var u={nodes:[],links:[]},s=n||i,f=t||a;return e.forEachNode(r),e.forEachLink(o),JSON.stringify(u)}n.exports=r},{}],28:[function(e,n,t){function r(e,n){var t=o(e);if(void 0===n)return t;for(var r=Object.keys(n),i=0;i<r.length;++i){var a=r[i],u=n[a];"link"===a?t.link(u):t.attr(a,u)}return t}function o(e){function n(e){return v||(v=i(p)),v.link(e),p}function t(e,n,t){return a.addEventListener(p,e,n,t),p}function o(e,n,t){return a.removeEventListener(p,e,n,t),p}function f(e){var n=r(e);return p.appendChild(n),n}function c(e,n){return 2===arguments.length?(null!==n?p.setAttributeNS(null,e,n):p.removeAttributeNS(null,e),p):p.getAttributeNS(null,e)}function d(e){return arguments.length?(p.setAttributeNS(s,"xlink:href",e),p):p.getAttributeNS(s,"xlink:href")}function l(e){return void 0!==e?(p.textContent=e,p):p.textContent}var p=e;if("string"==typeof e)p=window.document.createElementNS(u,e);else if(e.simplesvg)return e;var v;return p.simplesvg=!0,p.attr=c,p.append=f,p.link=d,p.text=l,p.on=t,p.off=o,p.dataSource=n,p}n.exports=r,r.compile=e("./lib/compile");var i=r.compileTemplate=e("./lib/compile_template"),a=e("add-event-listener"),u="http://www.w3.org/2000/svg",s="http://www.w3.org/1999/xlink"},{"./lib/compile":29,"./lib/compile_template":30,"add-event-listener":2}],29:[function(e,n,t){function r(e){try{return e=o(e),a(i.parseFromString(e,"text/xml").documentElement)}catch(n){throw n}}function o(e){if(e){var n='xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"',t=e.match(/^<\w+/);if(t){var r=t[0].length;return e.substr(0,r)+" "+n+" "+e.substr(r)}throw new Error("Cannot parse input text: invalid xml?")}}var i=e("./domparser.js"),a=e("../");n.exports=r},{"../":28,"./domparser.js":31}],30:[function(e,n,t){function r(e){var n=Object.create(null);return o(e,n),{link:function(e){function t(n){n(e)}Object.keys(n).forEach(function(e){var r=n[e];r.forEach(t)})}}}function o(e,n){var t=e.nodeType,r=1===t||3===t;if(r){var u;if(e.hasChildNodes()){var s=e.childNodes;for(u=0;u<s.length;++u)o(s[u],n)}if(3===t&&a(e,n),e.attributes){var f=e.attributes;for(u=0;u<f.length;++u)i(f[u],e,n)}}}function i(e,n,t){function r(e){n.setAttributeNS(null,a,e[s])}var o=e.value;if(o){var i=o.match(u);if(i){var a=e.localName,s=i[1],f=s.indexOf(".")<0;if(!f)throw new Error("simplesvg currently does not support nested bindings");var c=t[s];c?c.push(r):c=t[s]=[r]}}}function a(e,n){function t(n){e.nodeValue=n[i]}var r=e.nodeValue;if(r){var o=r.match(u);if(o){var i=o[1],a=(i.indexOf(".")<0,n[i]);a?a.push(t):a=n[i]=[t]}}}n.exports=r;var u=/{{(.+?)}}/},{}],31:[function(e,n,t){function r(){return"undefined"==typeof DOMParser?{parseFromString:o}:new DOMParser}function o(){throw new Error("DOMParser is not supported by this platform. Please open issue here https://github.com/anvaka/simplesvg")}n.exports=r()},{}],32:[function(e,n,t){function r(){return{betweennessCentrality:o,degreeCentrality:i}}function o(e){var n=u.betweenness(e);return a(n)}function i(e,n){var t=u.degree(e,n);return a(t)}function a(e){function n(n,t){return e[t]-e[n]}function t(n){return{key:n,value:e[n]}}return Object.keys(e).sort(n).map(t)}var u=e("ngraph.centrality");n.exports=r},{"ngraph.centrality":4}],33:[function(e,n,t){function r(){return{density:function(e,n){var t=e.getNodesCount();return 0===t?NaN:n?e.getLinksCount()/(t*(t-1)):2*e.getLinksCount()/(t*(t-1))}}}n.exports=r},{}],34:[function(e,n,t){function r(e,n){function t(e,t){var i;if(t){var a=n.getNodeUI(e.id);i=o(a),"function"==typeof t.onStart&&i.onStart(t.onStart),"function"==typeof t.onDrag&&i.onDrag(t.onDrag),"function"==typeof t.onStop&&i.onStop(t.onStop),r[e.id]=i}else(i=r[e.id])&&(i.release(),delete r[e.id])}var r={};return{bindDragNDrop:t}}n.exports=r;var o=e("./dragndrop.js")},{"./dragndrop.js":35}],35:[function(e,n,t){function r(e){var n,t,r,u,s,f,c,d=0,l=0,p=!1,v=0,g=function(e){var n=0,t=0;return e=e||window.event,e.pageX||e.pageY?(n=e.pageX,t=e.pageY):(e.clientX||e.clientY)&&(n=e.clientX+window.document.body.scrollLeft+window.document.documentElement.scrollLeft,t=e.clientY+window.document.body.scrollTop+window.document.documentElement.scrollTop),[n,t]},h=function(e,n,r){t&&t(e,{x:n-d,y:r-l}),d=n,l=r},m=function(e){e.stopPropagation?e.stopPropagation():e.cancelBubble=!0},y=function(e){e.preventDefault&&e.preventDefault()},x=function(e){return m(e),!1},w=function(e){e=e||window.event,h(e,e.clientX,e.clientY)},b=function(e){if(e=e||window.event,p)return m(e),!1;var t=1===e.button&&null!==window.event||0===e.button;return t?(d=e.clientX,l=e.clientY,c=e.target||e.srcElement,n&&n(e,{x:d,y:l}),o.on("mousemove",w),o.on("mouseup",E),m(e),s=window.document.onselectstart,f=window.document.ondragstart,window.document.onselectstart=x,c.ondragstart=x,!1):void 0},E=function(e){e=e||window.event,o.off("mousemove",w),o.off("mouseup",E),window.document.onselectstart=s,c.ondragstart=f,c=null,r&&r(e)},L=function(n){if("function"==typeof u){n=n||window.event,n.preventDefault&&n.preventDefault(),n.returnValue=!1;var t,r=g(n),o=a(e),i={x:r[0]-o[0],y:r[1]-o[1]};t=n.wheelDelta?n.wheelDelta/360:n.detail/-9,u(n,t,i)}},N=function(n){!u&&n?"webkit"===i.browser?e.addEventListener("mousewheel",L,!1):e.addEventListener("DOMMouseScroll",L,!1):u&&!n&&("webkit"===i.browser?e.removeEventListener("mousewheel",L,!1):e.removeEventListener("DOMMouseScroll",L,!1)),u=n},P=function(e,n){return(e.clientX-n.clientX)*(e.clientX-n.clientX)+(e.clientY-n.clientY)*(e.clientY-n.clientY)},k=function(e){if(1===e.touches.length){m(e);var n=e.touches[0];h(e,n.clientX,n.clientY)}else if(2===e.touches.length){var t=P(e.touches[0],e.touches[1]),r=0;t<v?r=-1:t>v&&(r=1),u(e,r,{x:e.touches[0].clientX,y:e.touches[0].clientY}),v=t,m(e),y(e)}},j=function(e){p=!1,o.off("touchmove",k),o.off("touchend",j),o.off("touchcancel",j),c=null,r&&r(e)},A=function(e,t){m(e),y(e),d=t.clientX,l=t.clientY,c=e.target||e.srcElement,n&&n(e,{x:d,y:l}),p||(p=!0,o.on("touchmove",k),o.on("touchend",j),o.on("touchcancel",j))},_=function(e){return 1===e.touches.length?A(e,e.touches[0]):void(2===e.touches.length&&(m(e),
+y(e),v=P(e.touches[0],e.touches[1])))};return e.addEventListener("mousedown",b),e.addEventListener("touchstart",_),{onStart:function(e){return n=e,this},onDrag:function(e){return t=e,this},onStop:function(e){return r=e,this},onScroll:function(e){return N(e),this},release:function(){e.removeEventListener("mousedown",b),e.removeEventListener("touchstart",_),o.off("mousemove",w),o.off("mouseup",E),o.off("touchmove",k),o.off("touchend",j),o.off("touchcancel",j),N(null)}}}n.exports=r;var o=e("../Utils/documentEvents.js"),i=e("../Utils/browserInfo.js"),a=e("../Utils/findElementPosition.js")},{"../Utils/browserInfo.js":39,"../Utils/documentEvents.js":40,"../Utils/findElementPosition.js":41}],36:[function(e,n,t){function r(e,n){var t=o(n),r=null,i={},a={x:0,y:0};return t.mouseDown(function(e,n){r=e,a.x=n.clientX,a.y=n.clientY,t.mouseCapture(r);var o=i[e.id];return o&&o.onStart&&o.onStart(n,a),!0}).mouseUp(function(e){t.releaseMouseCapture(r),r=null;var n=i[e.id];return n&&n.onStop&&n.onStop(),!0}).mouseMove(function(e,n){if(r){var t=i[r.id];return t&&t.onDrag&&t.onDrag(n,{x:n.clientX-a.x,y:n.clientY-a.y}),a.x=n.clientX,a.y=n.clientY,!0}}),{bindDragNDrop:function(e,n){i[e.id]=n,n||delete i[e.id]}}}n.exports=r;var o=e("../WebGL/webglInputEvents.js")},{"../WebGL/webglInputEvents.js":57}],37:[function(e,n,t){function r(e,n){function t(e){return d[e]}n=o(n,{maxX:1024,maxY:1024,seed:"Deterministic randomness made me do this"});var r=i(n.seed),u=new a(Number.MAX_VALUE,Number.MAX_VALUE,Number.MIN_VALUE,Number.MIN_VALUE),s={},f=function(e){return{x:r.next(n.maxX),y:r.next(n.maxY)}},c=function(e,n){e.x<n.x1&&(n.x1=e.x),e.x>n.x2&&(n.x2=e.x),e.y<n.y1&&(n.y1=e.y),e.y>n.y2&&(n.y2=e.y)},d="function"==typeof Object.create?Object.create(null):{},l=function(e){d[e.id]=f(e),c(d[e.id],u)},p=function(){0!==e.getNodesCount()&&(u.x1=Number.MAX_VALUE,u.y1=Number.MAX_VALUE,u.x2=Number.MIN_VALUE,u.y2=Number.MIN_VALUE,e.forEachNode(l))},v=function(e){s[e.id]=e},g=function(e){for(var n=0;n<e.length;++n){var t=e[n];t.node&&("add"===t.changeType?l(t.node):delete d[t.node.id]),t.link&&("add"===t.changeType?v(t.link):delete s[t.link.id])}};return e.forEachNode(l),e.forEachLink(v),e.on("changed",g),{run:function(e){this.step()},step:function(){return p(),!0},getGraphRect:function(){return u},dispose:function(){e.off("change",g)},isNodePinned:function(e){return!0},pinNode:function(e,n){},getNodePosition:t,getLinkPosition:function(e){var n=s[e];return{from:t(n.fromId),to:t(n.toId)}},setNodePosition:function(e,n,t){var r=d[e];r&&(r.x=n,r.y=t)},placeNode:function(e){return"function"==typeof e?(f=e,p(),this):f(e)}}}n.exports=r;var o=e("ngraph.merge"),i=e("ngraph.random").random,a=e("../Utils/rect.js")},{"../Utils/rect.js":45,"ngraph.merge":13,"ngraph.random":26}],38:[function(e,n,t){function r(e){function n(){var n=o(e);return n.addEventListener=n.on,n}if(console.log("This method is deprecated. Please use Viva.events() instead"),!e)return e;var t=void 0!==e.on||void 0!==e.off||void 0!==e.fire;return t?{extend:function(){return e},on:e.on,stop:e.off}:{extend:n,on:e.on,stop:e.off}}var o=e("ngraph.events");n.exports=r},{"ngraph.events":7}],39:[function(e,n,t){function r(){if("undefined"==typeof window||!window.hasOwnProperty("navigator"))return{browser:"",version:"0"};var e=window.navigator.userAgent.toLowerCase(),n=/(webkit)[ \/]([\w.]+)/,t=/(opera)(?:.*version)?[ \/]([\w.]+)/,r=/(msie) ([\w.]+)/,o=/(mozilla)(?:.*? rv:([\w.]+))?/,i=n.exec(e)||t.exec(e)||r.exec(e)||e.indexOf("compatible")<0&&o.exec(e)||[];return{browser:i[1]||"",version:i[2]||"0"}}n.exports=r()},{}],40:[function(e,n,t){function r(){return void 0===typeof document?a:{on:o,off:i}}function o(e,n){document.addEventListener(e,n)}function i(e,n){document.removeEventListener(e,n)}var a=e("./nullEvents.js");n.exports=r()},{"./nullEvents.js":44}],41:[function(e,n,t){function r(e){var n=0,t=0;if(e.offsetParent)do n+=e.offsetLeft,t+=e.offsetTop;while(null!==(e=e.offsetParent));return[n,t]}n.exports=r},{}],42:[function(e,n,t){function r(e){if(!e)throw{message:"Cannot get dimensions of undefined container"};var n=e.clientWidth,t=e.clientHeight;return{left:0,top:0,width:n,height:t}}n.exports=r},{}],43:[function(e,n,t){function r(e,n,t,r,i,a,u,s){return o(e,n,e,r,i,a,u,s)||o(e,r,t,r,i,a,u,s)||o(t,r,t,n,i,a,u,s)||o(t,n,e,n,i,a,u,s)}var o=e("gintersect");n.exports=r},{gintersect:3}],44:[function(e,n,t){function r(){return{on:o,off:o,stop:o}}function o(){}n.exports=r()},{}],45:[function(e,n,t){function r(e,n,t,r){this.x1=e||0,this.y1=n||0,this.x2=t||0,this.y2=r||0}n.exports=r},{}],46:[function(e,n,t){(function(e){function t(){function n(e){function n(){o=a.requestAnimationFrame(n),e()||t()}function t(){a.cancelAnimationFrame(o),o=0}function r(){o||n()}var o;return n(),{stop:t,restart:r}}function t(e){var n=(new Date).getTime(),t=Math.max(0,16-(n-u)),r=a.setTimeout(function(){e(n+t)},t);return u=n+t,r}function o(e){a.clearTimeout(e)}var i,a,u=0,s=["ms","moz","webkit","o"];for(a="undefined"!=typeof window?window:"undefined"!=typeof e?e:{setTimeout:r,clearTimeout:r},i=0;i<s.length&&!a.requestAnimationFrame;++i){var f=s[i];a.requestAnimationFrame=a[f+"RequestAnimationFrame"],a.cancelAnimationFrame=a[f+"CancelAnimationFrame"]||a[f+"CancelRequestAnimationFrame"]}return a.requestAnimationFrame||(a.requestAnimationFrame=t),a.cancelAnimationFrame||(a.cancelAnimationFrame=o),n}function r(){}n.exports=t()}).call(this,"undefined"!=typeof global?global:"undefined"!=typeof self?self:"undefined"!=typeof window?window:{})},{}],47:[function(e,n,t){function r(){return"undefined"==typeof window?a:{on:o,off:i}}function o(e,n){window.addEventListener(e,n)}function i(e,n){window.removeEventListener(e,n)}var a=e("./nullEvents.js");n.exports=r()},{"./nullEvents.js":44}],48:[function(e,n,t){function r(e,n){function t(e){return"string"==typeof z?z.indexOf(e)>=0:"boolean"!=typeof z||z}function r(){G=G||window.document.body,F=F||i(e,{springLength:80,springCoeff:2e-4}),O=O||a(e,{container:G}),n.hasOwnProperty("renderLinks")||(n.renderLinks=!0),n.prerender=n.prerender||0,U=(O.inputManager||s)(e,O)}function l(){O.beginRender(),n.renderLinks&&O.renderLinks(),O.renderNodes(),O.endRender()}function p(){return W=F.step()&&!H,l(),!W}function v(e){return R?void(V+=e):void(e?(V+=e,R=f(function(){return p()},M)):(X=0,V=0,R=f(p,M)))}function g(){J||(W=!1,R.restart())}function h(){if("number"==typeof n.prerender&&n.prerender>0)for(var e=0;e<n.prerender;e+=1)F.step()}function m(){var e=F.getGraphRect(),n=c(G),t=(e.x2+e.x1)/2,r=(e.y2+e.y1)/2;$.offsetX=n.width/2-(t*$.scale-t),$.offsetY=n.height/2-(r*$.scale-r),O.graphCenterChanged($.offsetX,$.offsetY),Y=!1}function y(e){var n=F.getNodePosition(e.id);O.addNode(e,n)}function x(e){O.releaseNode(e)}function w(e){var n=F.getLinkPosition(e.id);O.addLink(e,n)}function b(e){O.releaseLink(e)}function E(e){if(t("node")){var n=!1;U.bindDragNDrop(e,{onStart:function(){n=F.isNodePinned(e),F.pinNode(e,!0),H=!0,g()},onDrag:function(n,t){var r=F.getNodePosition(e.id);F.setNodePosition(e.id,r.x+t.x/$.scale,r.y+t.y/$.scale),H=!0,l()},onStop:function(){F.pinNode(e,n),H=!1}})}}function L(e){U.bindDragNDrop(e,null)}function N(){O.init(G),e.forEachNode(y),n.renderLinks&&e.forEachLink(w)}function P(){O.release(G)}function k(n){var t=n.node;"add"===n.changeType?(y(t),E(t),Y&&m()):"remove"===n.changeType?(L(t),x(t),0===e.getNodesCount()&&(Y=!0)):"update"===n.changeType&&(L(t),x(t),y(t),E(t))}function j(e){var t=e.link;if("add"===e.changeType)n.renderLinks&&w(t);else if("remove"===e.changeType)n.renderLinks&&b(t);else if("update"===e.changeType)throw"Update type is not implemented. TODO: Implement me!"}function A(e){var n,t;for(n=0;n<e.length;n+=1)t=e[n],t.node?k(t):t.link&&j(t);g()}function _(){m(),p()}function I(){B&&(B.release(),B=null)}function T(){e.off("changed",A)}function C(e,n){if(!n){var t=c(G);n={x:t.width/2,y:t.height/2}}var r=Math.pow(1.4,e?-.2:.2);return $.scale=O.scale(r,n),l(),K.fire("scale",$.scale),$.scale}function S(){u.on("resize",_),I(),t("drag")&&(B=d(G),B.onDrag(function(e,n){O.translateRel(n.x,n.y),l(),K.fire("drag",n)})),t("scroll")&&(B||(B=d(G)),B.onScroll(function(e,n,t){C(n<0,t)})),e.forEachNode(E),T(),e.on("changed",A)}function D(){q=!1,T(),I(),u.off("resize",_),K.off(),R.stop(),e.forEachLink(function(e){n.renderLinks&&b(e)}),e.forEachNode(function(e){L(e),x(e)}),F.dispose(),P()}var M=30;n=n||{};var U,R,B,F=n.layout,O=n.graphics,G=n.container,z=void 0===n.interactive||n.interactive,q=!1,Y=!0,X=0,V=0,W=!1,H=!1,J=!1,$={offsetX:0,offsetY:0,scale:1},K=o({});return{run:function(e){return q||(r(),h(),N(),m(),S(),q=!0),v(e),this},reset:function(){O.resetScale(),m(),$.scale=1},pause:function(){J=!0,R.stop()},resume:function(){J=!1,R.restart()},rerender:function(){return l(),this},zoomOut:function(){return C(!0)},zoomIn:function(){return C(!1)},moveTo:function(e,n){O.graphCenterChanged($.offsetX-e*$.scale,$.offsetY-n*$.scale),l()},getGraphics:function(){return O},dispose:function(){D()},on:function(e,n){return K.on(e,n),this},off:function(e,n){return K.off(e,n),this}}}n.exports=r;var o=e("ngraph.events"),i=e("ngraph.forcelayout"),a=e("./svgGraphics.js"),u=e("../Utils/windowEvents.js"),s=e("../Input/domInputManager.js"),f=e("../Utils/timer.js"),c=e("../Utils/getDimensions.js"),d=e("../Input/dragndrop.js")},{"../Input/domInputManager.js":34,"../Input/dragndrop.js":35,"../Utils/getDimensions.js":42,"../Utils/timer.js":46,"../Utils/windowEvents.js":47,"./svgGraphics.js":49,"ngraph.events":7,"ngraph.forcelayout":9}],49:[function(e,n,t){function r(){function e(){var e=o("svg");return n=o("g").attr("buffered-rendering","dynamic"),e.appendChild(n),e}var n,t,r,u=0,s=0,f=1,c={},d={},l=function(e){return o("rect").attr("width",10).attr("height",10).attr("fill","#00a2e8")},p=function(e,n){e.attr("x",n.x-5).attr("y",n.y-5)},v=function(e){return o("line").attr("stroke","#999")},g=function(e,n,t){e.attr("x1",n.x).attr("y1",n.y).attr("x2",t.x).attr("y2",t.y)},h=function(e){e.fire("rescaled")},m={x:0,y:0},y={x:0,y:0},x={x:0,y:0},w=function(){if(n){var e="matrix("+f+", 0, 0,"+f+","+u+","+s+")";n.attr("transform",e)}};t=e();var b={getNodeUI:function(e){return c[e]},getLinkUI:function(e){return d[e]},node:function(e){if("function"==typeof e)return l=e,this},link:function(e){if("function"==typeof e)return v=e,this},placeNode:function(e){return p=e,this},placeLink:function(e){return g=e,this},beginRender:function(){},endRender:function(){},graphCenterChanged:function(e,n){u=e,s=n,w()},inputManager:a,translateRel:function(e,r){var o=t.createSVGPoint(),i=n.getCTM(),a=t.createSVGPoint().matrixTransform(i.inverse());o.x=e,o.y=r,o=o.matrixTransform(i.inverse()),o.x=(o.x-a.x)*i.a,o.y=(o.y-a.y)*i.d,i.e+=o.x,i.f+=o.y;var u="matrix("+i.a+", 0, 0,"+i.d+","+i.e+","+i.f+")";n.attr("transform",u)},scale:function(e,r){var o=t.createSVGPoint();o.x=r.x,o.y=r.y,o=o.matrixTransform(n.getCTM().inverse());var i=t.createSVGMatrix().translate(o.x,o.y).scale(e).translate(-o.x,-o.y),a=n.getCTM().multiply(i);f=a.a,u=a.e,s=a.f;var c="matrix("+a.a+", 0, 0,"+a.d+","+a.e+","+a.f+")";return n.attr("transform",c),h(this),f},resetScale:function(){f=1;var e="matrix(1, 0, 0, 1, 0, 0)";return n.attr("transform",e),h(this),this},init:function(e){e.appendChild(t),w(),"function"==typeof r&&r(t)},release:function(e){t&&e&&e.removeChild(t)},addLink:function(e,t){var r=v(e);if(r)return r.position=t,r.link=e,d[e.id]=r,n.childElementCount>0?n.insertBefore(r,n.firstChild):n.appendChild(r),r},releaseLink:function(e){var t=d[e.id];t&&(n.removeChild(t),delete d[e.id])},addNode:function(e,t){var r=l(e);if(r)return r.position=t,r.node=e,c[e.id]=r,n.appendChild(r),r},releaseNode:function(e){var t=c[e.id];t&&(n.removeChild(t),delete c[e.id])},renderNodes:function(){for(var e in c)if(c.hasOwnProperty(e)){var n=c[e];m.x=n.position.x,m.y=n.position.y,p(n,m,n.node)}},renderLinks:function(){for(var e in d)if(d.hasOwnProperty(e)){var n=d[e];y.x=n.position.from.x,y.y=n.position.from.y,x.x=n.position.to.x,x.y=n.position.to.y,g(n,y,x,n.link)}},getGraphicsRoot:function(e){return"function"==typeof e&&(t?e(t):r=e),t},getSvgRoot:function(){return t}};return i(b),b}n.exports=r;var o=e("simplesvg"),i=e("ngraph.events"),a=e("../Input/domInputManager.js")},{"../Input/domInputManager.js":34,"ngraph.events":7,simplesvg:28}],50:[function(e,n,t){function r(e){e=c(e,{enableBlending:!0,preserveDrawingBuffer:!1,clearColor:!1,clearColorValue:{r:1,g:1,b:1,a:1}});var n,t,r,d,l,p,v,g,h=0,m=0,y=[1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1],x=[],w=[],b={},E={},L=i(),N=a(),P=function(e){return u()},k=function(e){return s(3014898687)},j=function(){L.updateTransform(y),N.updateTransform(y)},A=function(){y=[1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1]},_=function(){n&&t&&(d=t.width=Math.max(n.offsetWidth,1),l=t.height=Math.max(n.offsetHeight,1),r&&r.viewport(0,0,d,l),L&&L.updateSize(d/2,l/2),N&&N.updateSize(d/2,l/2))},I=function(e){e.fire("rescaled")};t=window.document.createElement("canvas");var T={getLinkUI:function(e){return E[e]},getNodeUI:function(e){return b[e]},node:function(e){if("function"==typeof e)return P=e,this},link:function(e){if("function"==typeof e)return k=e,this},placeNode:function(e){return p=e,this},placeLink:function(e){return v=e,this},inputManager:o,beginRender:function(){},endRender:function(){m>0&&L.render(),h>0&&N.render()},bringLinkToFront:function(e){var n,t,r=L.getFrontLinkId();L.bringToFront(e),r>e.id&&(n=e.id,t=w[r],w[r]=w[n],w[r].id=r,w[n]=t,w[n].id=n)},graphCenterChanged:function(e,n){y[12]=2*e/d-1,y[13]=1-2*n/l,j()},addLink:function(e,n){var t=m++,r=k(e);return r.id=t,r.pos=n,L.createLink(r),w[t]=r,E[e.id]=r,r},addNode:function(e,n){var t=h++,r=P(e);return r.id=t,r.position=n,r.node=e,N.createNode(r),x[t]=r,b[e.id]=r,r},translateRel:function(e,n){y[12]+=2*y[0]*e/d/y[0],y[13]-=2*y[5]*n/l/y[5],j()},scale:function(e,n){var t=2*n.x/d-1,r=1-2*n.y/l;return t-=y[12],r-=y[13],y[12]+=t*(1-e),y[13]+=r*(1-e),y[0]*=e,y[5]*=e,j(),I(this),y[0]},resetScale:function(){return A(),r&&(_(),j()),this},init:function(o){var i={};if(e.preserveDrawingBuffer&&(i.preserveDrawingBuffer=!0),n=o,_(),A(),n.appendChild(t),r=t.getContext("experimental-webgl",i),!r){var a="Could not initialize WebGL. Seems like the browser doesn't support it.";throw window.alert(a),a}if(e.enableBlending&&(r.blendFunc(r.SRC_ALPHA,r.ONE_MINUS_SRC_ALPHA),r.enable(r.BLEND)),e.clearColor){var u=e.clearColorValue;r.clearColor(u.r,u.g,u.b,u.a),this.beginRender=function(){r.clear(r.COLOR_BUFFER_BIT)}}L.load(r),L.updateSize(d/2,l/2),N.load(r),N.updateSize(d/2,l/2),j(),"function"==typeof g&&g(t)},release:function(e){t&&e&&e.removeChild(t)},isSupported:function(){var e=window.document.createElement("canvas"),n=e&&e.getContext&&e.getContext("experimental-webgl");return n},releaseLink:function(e){m>0&&(m-=1);var n=E[e.id];delete E[e.id],L.removeLink(n);var t=n.id;if(t<m){if(0===m||m===t)return;var r=w[m];w[t]=r,r.id=t}},releaseNode:function(e){h>0&&(h-=1);var n=b[e.id];delete b[e.id],N.removeNode(n);var t=n.id;if(t<h){if(0===h||h===t)return;var r=x[h];x[t]=r,r.id=t,N.replaceProperties(n,r)}},renderNodes:function(){for(var e={x:0,y:0},n=0;n<h;++n){var t=x[n];e.x=t.position.x,e.y=t.position.y,p&&p(t,e),N.position(t,e)}},renderLinks:function(){if(!this.omitLinksRendering)for(var e={x:0,y:0},n={x:0,y:0},t=0;t<m;++t){var r=w[t],o=r.pos.from;n.x=o.x,n.y=-o.y,o=r.pos.to,e.x=o.x,e.y=-o.y,v&&v(r,n,e),L.position(r,n,e)}},getGraphicsRoot:function(e){return"function"==typeof e&&(t?e(t):g=e),t},setNodeProgram:function(e){if(!r&&e)N=e;else if(e)throw"Not implemented. Cannot swap shader on the fly... Yet."},setLinkProgram:function(e){if(!r&&e)L=e;else if(e)throw"Not implemented. Cannot swap shader on the fly... Yet."},transformClientToGraphCoordinates:function(e){return e.x=2*e.x/d-1,e.y=1-2*e.y/l,e.x=(e.x-y[12])/y[0],e.y=(e.y-y[13])/y[5],e.x=e.x*(d/2),e.y=e.y*(-l/2),e},transformGraphToClientCoordinates:function(e){return e.x=e.x/(d/2),e.y=e.y/(-l/2),e.x=e.x*y[0]+y[12],e.y=e.y*y[5]+y[13],e.x=(e.x+1)*d/2,e.y=(1-e.y)*l/2,e},getNodeAtClientPos:function(e,n){if("function"!=typeof n)return null;this.transformClientToGraphCoordinates(e);for(var t=0;t<h;++t)if(n(x[t],e.x,e.y))return x[t].node;return null}};return f(T),T}n.exports=r;var o=e("../Input/webglInputManager.js"),i=e("../WebGL/webglLinkProgram.js"),a=e("../WebGL/webglNodeProgram.js"),u=e("../WebGL/webglSquare.js"),s=e("../WebGL/webglLine.js"),f=e("ngraph.events"),c=e("ngraph.merge")},{"../Input/webglInputManager.js":36,"../WebGL/webglLine.js":58,"../WebGL/webglLinkProgram.js":59,"../WebGL/webglNodeProgram.js":60,"../WebGL/webglSquare.js":61,"ngraph.events":7,"ngraph.merge":13}],51:[function(e,n,t){function r(e){var n=10414335;if("string"==typeof e&&e)if(4===e.length&&(e=e.replace(/([^#])/g,"$1$1")),9===e.length)n=parseInt(e.substr(1),16);else{if(7!==e.length)throw'Color expected in hex format with preceding "#". E.g. #00ff00. Got value: '+e;n=parseInt(e.substr(1),16)<<8|255}else"number"==typeof e&&(n=e);return n}n.exports=r},{}],52:[function(e,n,t){function r(e){this.canvas=window.document.createElement("canvas"),this.ctx=this.canvas.getContext("2d"),this.isDirty=!1,this.canvas.width=this.canvas.height=e}n.exports=r},{}],53:[function(e,n,t){function r(e){function n(n,t){var r=e.createShader(t);if(e.shaderSource(r,n),e.compileShader(r),!e.getShaderParameter(r,e.COMPILE_STATUS)){var o=e.getShaderInfoLog(r);throw window.alert(o),o}return r}function t(t,r){var o=e.createProgram(),i=n(t,e.VERTEX_SHADER),a=n(r,e.FRAGMENT_SHADER);if(e.attachShader(o,i),e.attachShader(o,a),e.linkProgram(o),!e.getProgramParameter(o,e.LINK_STATUS)){var u=e.getShaderInfoLog(o);throw window.alert(u),u}return o}function r(e,n,t){if((n+1)*t>e.length){var r=new Float32Array(e.length*t*2);return r.set(e),r}return e}function a(n,t){for(var r={},o=0;o<t.length;++o){var i=t[o],a=-1;if("a"===i[0]&&"_"===i[1]){if(a=e.getAttribLocation(n,i),a===-1)throw new Error("Program doesn't have required attribute: "+i);r[i.slice(2)]=a}else{if("u"!==i[0]||"_"!==i[1])throw new Error("Couldn't figure out your intent. All uniforms should start with 'u_' prefix, and attributes with 'a_'");if(a=e.getUniformLocation(n,i),null===a)throw new Error("Program doesn't have required uniform: "+i);r[i.slice(2)]=a}}return r}return{createProgram:t,extendArray:r,copyArrayPart:o,swapArrayPart:i,getLocations:a,context:e}}function o(e,n,t,r){for(var o=0;o<r;++o)e[n+o]=e[t+o]}function i(e,n,t,r){for(var o=0;o<r;++o){var i=e[n+o];e[n+o]=e[t+o],e[t+o]=i}}n.exports=r},{}],54:[function(e,n,t){function r(e){function n(){var e;for(E.isDirty=!1,e=0;e<w.length;++e)w[e].isDirty=!1}function t(e){var n=y[e];if(!n)return!1;if(delete y[e],m-=1,m===n.offset)return!0;var t=c(n.offset),r=c(m);p(r,t);var o=y[b[m]];return o.offset=n.offset,b[n.offset]=b[m],l(),!0}function r(){return w}function a(e){return y[e]}function u(e,n){if(y.hasOwnProperty(e))n(y[e]);else{var t=new window.Image,r=m;m+=1,t.crossOrigin="anonymous",t.onload=function(){l(),f(r,t,n)},t.src=e}}function s(){var e=new i(g*h);w.push(e)}function f(e,n,t){var r=c(e),o={offset:e};r.textureNumber>=w.length&&s();var i=w[r.textureNumber];i.ctx.drawImage(n,r.col*h,r.row*h,h,h),b[e]=n.src,y[n.src]=o,i.isDirty=!0,t(o)}function c(n){var t=n/e<<0,r=n%e,o=r/g<<0,i=r%g;return{textureNumber:t,row:o,col:i}}function d(){E.isDirty=!0,x=0,v=null}function l(){v&&(window.clearTimeout(v),x+=1,v=null),x>10?d():v=window.setTimeout(d,400)}function p(e,n){var t=w[e.textureNumber].canvas,r=w[n.textureNumber].ctx,o=n.col*h,i=n.row*h;r.drawImage(t,e.col*h,e.row*h,h,h,o,i,h,h),w[e.textureNumber].isDirty=!0,w[n.textureNumber].isDirty=!0}var v,g=Math.sqrt(e||1024)<<0,h=g,m=1,y={},x=0,w=[],b=[];if(!o(e))throw"Tiles per texture should be power of two.";var E={isDirty:!1,clearDirty:n,remove:t,getTextures:r,getCoordinates:a,load:u};return E}function o(e){return 0===(e&e-1)}var i=e("./texture.js");n.exports=r},{"./texture.js":52}],55:[function(e,n,t){function r(e,n){return{_texture:0,_offset:0,size:"number"==typeof e?e:32,src:n}}n.exports=r},{}],56:[function(e,n,t){function r(){function e(e,n){e.nativeObject&&h.deleteTexture(e.nativeObject);var t=h.createTexture();h.activeTexture(h["TEXTURE"+n]),h.bindTexture(h.TEXTURE_2D,t),h.texImage2D(h.TEXTURE_2D,0,h.RGBA,h.RGBA,h.UNSIGNED_BYTE,e.canvas),h.texParameteri(h.TEXTURE_2D,h.TEXTURE_MAG_FILTER,h.LINEAR),h.texParameteri(h.TEXTURE_2D,h.TEXTURE_MIN_FILTER,h.LINEAR_MIPMAP_NEAREST),h.generateMipmap(h.TEXTURE_2D),h.uniform1i(x["sampler"+n],n),e.nativeObject=t}function n(){if(v.isDirty){var n,t=v.getTextures();for(n=0;n<t.length;++n)!t[n].isDirty&&t[n].nativeObject||e(t[n],n);v.clearDirty()}}function t(e){h=e,y=u(e),v=new a(j),g=y.createProgram(k,P),h.useProgram(g),x=y.getLocations(g,["a_vertexPos","a_customAttributes","u_screenSize","u_transform","u_sampler0","u_sampler1","u_sampler2","u_sampler3","u_tilesPerTexture"]),h.uniform1f(x.tilesPerTexture,j),h.enableVertexAttribArray(x.vertexPos),h.enableVertexAttribArray(x.customAttributes),m=h.createBuffer()}function r(e,n){var t=e.id*N;_[t]=n.x-e.size,_[t+1]=n.y-e.size,_[t+2]=4*e._offset,_[t+3]=n.x+e.size,_[t+4]=n.y-e.size,_[t+5]=4*e._offset+1,_[t+6]=n.x-e.size,_[t+7]=n.y+e.size,_[t+8]=4*e._offset+2,_[t+9]=n.x-e.size,_[t+10]=n.y+e.size,_[t+11]=4*e._offset+2,_[t+12]=n.x+e.size,_[t+13]=n.y-e.size,_[t+14]=4*e._offset+1,_[t+15]=n.x+e.size,_[t+16]=n.y+e.size,_[t+17]=4*e._offset+3}function s(e){_=y.extendArray(_,A,N),A+=1;var n=v.getCoordinates(e.src);n?e._offset=n.offset:(e._offset=0,v.load(e.src,function(n){e._offset=n.offset}))}function f(e){A>0&&(A-=1),e.id<A&&A>0&&(e.src&&v.remove(e.src),y.copyArrayPart(_,e.id*N,A*N,N))}function c(e,n){n._offset=e._offset}function d(e){L=!0,E=e}function l(e,n){w=e,b=n,L=!0}function p(){h.useProgram(g),h.bindBuffer(h.ARRAY_BUFFER,m),h.bufferData(h.ARRAY_BUFFER,_,h.DYNAMIC_DRAW),L&&(L=!1,h.uniformMatrix4fv(x.transform,!1,E),h.uniform2f(x.screenSize,w,b)),h.vertexAttribPointer(x.vertexPos,2,h.FLOAT,!1,3*Float32Array.BYTES_PER_ELEMENT,0),h.vertexAttribPointer(x.customAttributes,1,h.FLOAT,!1,3*Float32Array.BYTES_PER_ELEMENT,8),n(),h.drawArrays(h.TRIANGLES,0,6*A)}var v,g,h,m,y,x,w,b,E,L,N=18,P=o(),k=i(),j=1024,A=0,_=new Float32Array(64);return{load:t,position:r,createNode:s,removeNode:f,replaceProperties:c,updateTransform:d,updateSize:l,render:p}}function o(){return["precision mediump float;","varying vec4 color;","varying vec3 vTextureCoord;","uniform sampler2D u_sampler0;","uniform sampler2D u_sampler1;","uniform sampler2D u_sampler2;","uniform sampler2D u_sampler3;","void main(void) {"," if (vTextureCoord.z == 0.) {"," gl_FragColor = texture2D(u_sampler0, vTextureCoord.xy);"," } else if (vTextureCoord.z == 1.) {"," gl_FragColor = texture2D(u_sampler1, vTextureCoord.xy);"," } else if (vTextureCoord.z == 2.) {"," gl_FragColor = texture2D(u_sampler2, vTextureCoord.xy);"," } else if (vTextureCoord.z == 3.) {"," gl_FragColor = texture2D(u_sampler3, vTextureCoord.xy);"," } else { gl_FragColor = vec4(0, 1, 0, 1); }","}"].join("\n")}function i(){return["attribute vec2 a_vertexPos;","attribute float a_customAttributes;","uniform vec2 u_screenSize;","uniform mat4 u_transform;","uniform float u_tilesPerTexture;","varying vec3 vTextureCoord;","void main(void) {"," gl_Position = u_transform * vec4(a_vertexPos/u_screenSize, 0, 1);","float corner = mod(a_customAttributes, 4.);","float tileIndex = mod(floor(a_customAttributes / 4.), u_tilesPerTexture);","float tilesPerRow = sqrt(u_tilesPerTexture);","float tileSize = 1./tilesPerRow;","float tileColumn = mod(tileIndex, tilesPerRow);","float tileRow = floor(tileIndex/tilesPerRow);","if(corner == 0.0) {"," vTextureCoord.xy = vec2(0, 1);","} else if(corner == 1.0) {"," vTextureCoord.xy = vec2(1, 1);","} else if(corner == 2.0) {"," vTextureCoord.xy = vec2(0, 0);","} else {"," vTextureCoord.xy = vec2(1, 0);","}","vTextureCoord *= tileSize;","vTextureCoord.x += tileColumn * tileSize;","vTextureCoord.y += tileRow * tileSize;","vTextureCoord.z = floor(floor(a_customAttributes / 4.)/u_tilesPerTexture);","}"].join("\n")}var a=e("./webglAtlas.js"),u=e("./webgl.js");n.exports=r},{"./webgl.js":53,"./webglAtlas.js":54}],57:[function(e,n,t){function r(e){function n(){x=null}function t(e){x=e}function r(e){return"function"==typeof e&&k.push(e),A}function i(e){return"function"==typeof e&&P.push(e),A}function a(e){return"function"==typeof e&&N.push(e),A}function u(e){return"function"==typeof e&&L.push(e),A}function s(e){return"function"==typeof e&&E.push(e),A}function f(e){return"function"==typeof e&&b.push(e),A}function c(e){return"function"==typeof e&&w.push(e),A}function d(e,n,t){if(e&&e.size){var r=e.position,o=e.size;return r.x-o<n&&n<r.x+o&&r.y-o<t&&t<r.y+o}return!0}function l(n){return e.getNodeAtClientPos(n,d)}function p(e){e.stopPropagation?e.stopPropagation():e.cancelBubble=!0}function v(e){return p(e),!1}function g(e,n){var t,r;for(t=0;t<e.length;t+=1)if(r=e[t].apply(void 0,n))return!0}function h(e){var n={x:0,y:0},t=null,r=1,i=+new Date,a=function(e){g(N,[t,e]),n.x=e.clientX,n.y=e.clientY},u=function(){o.off("mousemove",a),o.off("mouseup",u)},s=function(){y=e.getBoundingClientRect()};window.addEventListener("resize",s),s(),e.addEventListener("mousemove",function(e){if(!x){r++%7===0&&(s(),r=1);var o,i=!1;n.x=e.clientX-y.left,n.y=e.clientY-y.top,o=l(n),o&&t!==o?(t=o,i=i||g(w,[t])):null===o&&t!==o&&(i=i||g(b,[t]),t=null),i&&p(e)}}),e.addEventListener("mousedown",function(e){var r,i=!1;s(),n.x=e.clientX-y.left,n.y=e.clientY-y.top,r=[l(n),e],r[0]?(i=g(E,r),o.on("mousemove",a),o.on("mouseup",u),m=window.document.onselectstart,window.document.onselectstart=v,t=r[0]):t=null,i&&p(e)}),e.addEventListener("mouseup",function(e){var r,o=+new Date;n.x=e.clientX-y.left,n.y=e.clientY-y.top;var a=l(n),u=a===t;r=[a||t,e],r[0]&&(window.document.onselectstart=m,o-i<400&&u?g(k,r):g(P,r),i=o,g(L,r)&&p(e))})}if(e.webglInputEvents)return e.webglInputEvents;var m,y,x=null,w=[],b=[],E=[],L=[],N=[],P=[],k=[],j=e.getGraphicsRoot();h(j);var A={mouseEnter:c,mouseLeave:f,mouseDown:s,mouseUp:u,mouseMove:a,click:i,dblClick:r,mouseCapture:t,releaseMouseCapture:n};return e.webglInputEvents=A,A}var o=e("../Utils/documentEvents.js");n.exports=r},{"../Utils/documentEvents.js":40}],58:[function(e,n,t){function r(e){return{color:o(e)}}var o=e("./parseColor.js");n.exports=r},{"./parseColor.js":51}],59:[function(e,n,t){function r(){var e,n,t,r,i,a,u,s,f,c,d=6,l=2*(2*Float32Array.BYTES_PER_ELEMENT+Uint32Array.BYTES_PER_ELEMENT),p=["precision mediump float;","varying vec4 color;","void main(void) {"," gl_FragColor = color;","}"].join("\n"),v=["attribute vec2 a_vertexPos;","attribute vec4 a_color;","uniform vec2 u_screenSize;","uniform mat4 u_transform;","varying vec4 color;","void main(void) {"," gl_Position = u_transform * vec4(a_vertexPos/u_screenSize, 0.0, 1.0);"," color = a_color.abgr;","}"].join("\n"),g=0,h=new ArrayBuffer(16*l),m=new Float32Array(h),y=new Uint32Array(h),x=function(){if((g+1)*l>h.byteLength){var e=new ArrayBuffer(2*h.byteLength),n=new Float32Array(e),t=new Uint32Array(e);t.set(y),m=n,y=t,h=e}};return{load:function(a){n=a,r=o(a),e=r.createProgram(v,p),n.useProgram(e),i=r.getLocations(e,["a_vertexPos","a_color","u_screenSize","u_transform"]),n.enableVertexAttribArray(i.vertexPos),n.enableVertexAttribArray(i.color),t=n.createBuffer()},position:function(e,n,t){var r=e.id,o=r*d;m[o]=n.x,m[o+1]=n.y,y[o+2]=e.color,m[o+3]=t.x,m[o+4]=t.y,y[o+5]=e.color},createLink:function(e){x(),g+=1,a=e.id},removeLink:function(e){g>0&&(g-=1),e.id<g&&g>0&&r.copyArrayPart(y,e.id*d,g*d,d)},updateTransform:function(e){c=!0,f=e},updateSize:function(e,n){u=e,s=n,c=!0},render:function(){n.useProgram(e),n.bindBuffer(n.ARRAY_BUFFER,t),n.bufferData(n.ARRAY_BUFFER,h,n.DYNAMIC_DRAW),c&&(c=!1,n.uniformMatrix4fv(i.transform,!1,f),n.uniform2f(i.screenSize,u,s)),n.vertexAttribPointer(i.vertexPos,2,n.FLOAT,!1,3*Float32Array.BYTES_PER_ELEMENT,0),n.vertexAttribPointer(i.color,4,n.UNSIGNED_BYTE,!0,3*Float32Array.BYTES_PER_ELEMENT,8),n.drawArrays(n.LINES,0,2*g),a=g-1},bringToFront:function(e){a>e.id&&r.swapArrayPart(m,e.id*d,a*d,d),a>0&&(a-=1)},getFrontLinkId:function(){return a}}}var o=e("./webgl.js");n.exports=r},{"./webgl.js":53}],60:[function(e,n,t){function r(){function e(){if((k+1)*w>=L.byteLength){var e=new ArrayBuffer(2*L.byteLength),n=new Float32Array(e),t=new Uint32Array(e);t.set(P),N=n,P=t,L=e}}function n(e){d=e,v=o(e),c=v.createProgram(E,b),d.useProgram(c),p=v.getLocations(c,["a_vertexPos","a_color","u_screenSize","u_transform"]),d.enableVertexAttribArray(p.vertexPos),d.enableVertexAttribArray(p.color),l=d.createBuffer()}function t(e,n){var t=e.id;N[t*x]=n.x,N[t*x+1]=-n.y,N[t*x+2]=e.size,P[t*x+3]=e.color}function r(e){y=!0,m=e}function i(e,n){g=e,h=n,y=!0}function a(e){k>0&&(k-=1),e.id<k&&k>0&&v.copyArrayPart(P,e.id*x,k*x,x)}function u(){e(),k+=1}function s(){}function f(){d.useProgram(c),d.bindBuffer(d.ARRAY_BUFFER,l),d.bufferData(d.ARRAY_BUFFER,L,d.DYNAMIC_DRAW),y&&(y=!1,d.uniformMatrix4fv(p.transform,!1,m),d.uniform2f(p.screenSize,g,h)),d.vertexAttribPointer(p.vertexPos,3,d.FLOAT,!1,x*Float32Array.BYTES_PER_ELEMENT,0),d.vertexAttribPointer(p.color,4,d.UNSIGNED_BYTE,!0,x*Float32Array.BYTES_PER_ELEMENT,12),d.drawArrays(d.POINTS,0,k)}var c,d,l,p,v,g,h,m,y,x=4,w=3*Float32Array.BYTES_PER_ELEMENT+Uint32Array.BYTES_PER_ELEMENT,b=["precision mediump float;","varying vec4 color;","void main(void) {"," gl_FragColor = color;","}"].join("\n"),E=["attribute vec3 a_vertexPos;","attribute vec4 a_color;","uniform vec2 u_screenSize;","uniform mat4 u_transform;","varying vec4 color;","void main(void) {"," gl_Position = u_transform * vec4(a_vertexPos.xy/u_screenSize, 0, 1);"," gl_PointSize = a_vertexPos.z * u_transform[0][0];"," color = a_color.abgr;","}"].join("\n"),L=new ArrayBuffer(16*w),N=new Float32Array(L),P=new Uint32Array(L),k=0;return{load:n,position:t,updateTransform:r,updateSize:i,removeNode:a,createNode:u,replaceProperties:s,render:f}}var o=e("./webgl.js");n.exports=r},{"./webgl.js":53}],61:[function(e,n,t){function r(e,n){return{size:"number"==typeof e?e:10,color:o(n)}}var o=e("./parseColor.js");n.exports=r},{"./parseColor.js":51}],62:[function(e,n,t){n.exports="0.8.1"},{}]},{},[1])(1)}); \ No newline at end of file
diff --git a/templates/error.jinja b/templates/error.jinja
new file mode 100644
index 0000000..60c185f
--- /dev/null
+++ b/templates/error.jinja
@@ -0,0 +1,16 @@
+<html>
+ <head>
+ <title>CS:GO Lobbylinkfinder</title>
+ <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+ <link rel="stylesheet" type="text/css" href="static/style.css" />
+ </head>
+ <body>
+ <div class="error">
+ <h4>Es ist ein Fehler aufgetreten!</h4>
+ <pre>{{ error }}</pre>
+ <a href="javascript:window.history.back();" class="joinbutton">Zur&uuml;ck</a>
+ <a href="/" class="joinbutton">Zur Startseite</a>
+ </div>
+ </body>
+</html>
+
diff --git a/templates/lobby_html.jinja b/templates/lobby_html.jinja
new file mode 100644
index 0000000..fcf440b
--- /dev/null
+++ b/templates/lobby_html.jinja
@@ -0,0 +1,100 @@
+<html><head>
+ <title>CS:GO Lobbylinkfinder</title>
+ <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+ <link rel="stylesheet" type="text/css" href="static/style.css" />
+ <script src="static/hide.js"></script>
+ </head>
+ <body>
+
+ <ul id="menu">
+ <li class="menu"><a class="joinbutton" href="javascript:hideOffline();" id="offlinetoggle">Hide Offline</a></li>
+ <li class="menu"><a href="javascript:location.reload();" class="joinbutton">Refresh</a></li>
+ <li class="menu app"><a class="joinbutton" href="premadefinder">Premadefinder</a></li>
+ </ul>
+
+ {% with messages = get_flashed_messages() %}
+ {% if messages %}
+ <ul id="flash">
+ {% for message in messages %}
+ <li>{{ message }}</li>
+ {% endfor %}
+ </ul>
+ {% endif %}
+ {% endwith %}
+
+ {# avatar: 32x32, avatarmedium: 64x64, avatarfull: 184x184 #}
+
+ {% for steamid,profile in steamids.items() %}
+ {% if profile['gameextrainfo'] %}{% set state = "Ingame" %}{% else %}{% set state = states[profile['personastate']].lower().replace(' ', '') %}{% endif %}
+
+ <div class="player {{ state }}">
+ <div class="interna">
+ <a href="{{ profile['profileurl'] }}">
+ <span class="steamname {{ state }}">{{ profile['personaname']|e }}</span><br />
+ <img src="{{ profile['avatarfull'] }}" class="avatar {{ state }}" /><br />
+ <span class="maininfo">({{ profile['main']|e }})</span>
+ </a>
+ </div>
+ <div class="interna stats">
+ {# Ingame (Steam), Ingame (Nonsteam), Offline/Other #}
+ {% if profile['gameid'] %}
+ <span class="ingame"><a href="http://store.steampowered.com/app/{{ profile['gameid'] }}" class="ingame">{{ profile['gameextrainfo'] }}
+ {% if profile['gameserverip'] not in serverinfo %}
+ {# No serverinfo -> Display game logo #}
+ <img src="http://cdn.akamai.steamstatic.com/steam/apps/{{ profile['gameid'] }}/header.jpg" class="gameimage" />
+ {% endif %}
+ </a></span><br/>
+ {% if profile['lobbysteamid'] %}
+ <div class="buttons">
+ <a href="steam://joinlobby/{{ profile['gameid'] }}/{{ profile['lobbysteamid'] }}/{{ profile['steamid'] }}" class="joinbutton">Join lobby</a>
+ <a href="#" onclick="javascript:window.prompt('Lobbylink', '[url]steam://joinlobby/{{ profile['gameid'] }}/{{ profile['lobbysteamid'] }}/{{ profile['steamid'] }}[/url]'); return false;" class="joinbutton">Copy link</a>
+ </div>
+ {% elif profile['gameserverip'] %}
+ {# playing on a server, got info? #}
+ {% if serverinfo[profile['gameserverip']] %}
+ <div class="serverinfo">
+ {% set game = serverinfo[profile['gameserverip']]['game'] %}
+ {% set servername = serverinfo[profile['gameserverip']]['name'] %}
+ {% set mapname = serverinfo[profile['gameserverip']]['map'] %}
+ {% set players = serverinfo[profile['gameserverip']]['players'] %}
+ {% set playersmax = serverinfo[profile['gameserverip']]['playersmax'] %}
+ {% if game != profile['gameextrainfo'] %}
+ <span class="gamename">{{ game }}</span><br />
+ {% endif %}
+ {{ servername }}<br />
+ {{ mapname }} ({{ players }}/{{ playersmax }})
+ </div>
+ {% endif %}
+ <div class="buttons">
+ <a href="steam://connect/{{ profile['gameserverip'] }}" class="joinbutton">Connect</a>
+ <a href="#" onclick="javascript:window.prompt('Lobbylink', '[url]steam://connect/{{ profile['gameserverip'] }}[/url]'); return false;" class="joinbutton">Copy link</a>
+ </div>
+ {% endif %}
+ {% elif profile['gameextrainfo'] %}
+ <span class="ingameother">{{ profile['gameextrainfo'] }}</span><br />
+ <span class="info">(non-steam)</span>
+ {% else %}
+ {% if profile['personastate'] == 0 %}
+ <span class="{{ state }}">Offline since {{ display_time(current_time - profile['lastlogoff']) }}</span>
+ {% else %}
+ <span class="{{ state }}">{{ states[profile['personastate']] }}</span>
+ {% endif %}
+ {% if profile['communityvisibilitystate'] == 1 %}
+ <br /><span class="error">Profile private</span>
+ {% endif %}
+ {% endif %}
+ </div>
+ </div>
+ {% endfor %}
+
+ {% if steamids['76561197963063991'] %}
+ <p id="disclaimer">
+ Falls dir hier Accounts fehlen oder du Vorschläge hast oder hier nicht genannt werden willst,
+ dann wende dich an <a class="{{ states[ steamids['76561197963063991']['personastate'] ] }}" href="{{ steamids['76561197963063991']['profileurl'] }}"><img src="{{ steamids['76561197963063991']['avatar'] }}" width="16" height="16 "/>Penguin</a>!
+ </p>
+ {% endif %}
+
+ </body>
+</html>
+
+<!-- vim: commentstring={#\ %s\ #}
diff --git a/templates/premades_html.jinja b/templates/premades_html.jinja
new file mode 100644
index 0000000..39ebb0b
--- /dev/null
+++ b/templates/premades_html.jinja
@@ -0,0 +1,102 @@
+<html><head>
+ <title>CS:GO Premadefinder</title>
+ <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+ <link rel="stylesheet" type="text/css" href="static/style.css" />
+ <script src="static/vivagraph.min.js"></script>
+ </head>
+ <body onload="onLoad();">
+
+ <ul id="menu">
+ <li class="menu app"><a class="joinbutton" href="lobby">Lobbylinkfinder</a></li>
+ </ul>
+
+ <div class="form">
+ Copy&amp;Paste den Inhalt von 'status' aus der Konsole.<br />
+ <form action="premadefinder" method="post">
+ <textarea name="statustext" cols="75" rows="10"></textarea><br />
+ <input class="joinbutton" type="submit" name="Freunde finden!" />
+ </form>
+ </div>
+
+
+ {% if steamids %}
+
+ <script type="text/javascript">
+ function onLoad(){
+ var graph = Viva.Graph.graph();
+
+ {% for steamid in steamids %}
+ graph.addNode('{{ steamid }}', {url : '{{ profiles[steamid]['avatarmedium'] }}', name : '{{ profiles[steamid]['personaname']|e }}', friends : {% if profiles[steamid]['communityvisibilitystate'] == 3 %} 'yes' {% else %} 'no' {% endif %}});
+ {% endfor %}
+ {% for connection in connections %}
+ graph.addLink('{{ connection[0] }}', '{{ connection[1] }}');
+ {% endfor %}
+
+ var graphics = Viva.Graph.View.svgGraphics();
+ graphics.node(
+ function(node) {
+ if (node.data) {
+ var ui = Viva.Graph.svg('g'),
+ svgText = Viva.Graph.svg('text')
+ .attr('text-anchor', 'middle')
+ .attr('fill', 'rgb(150, 150, 0)')
+ .attr('x', '+16px')
+ .attr('y', '+48px')
+ .text(node.data.name),
+ img = Viva.Graph.svg('image')
+ .attr('width', 32)
+ .attr('height', 32)
+ .link(node.data.url),
+ border = Viva.Graph.svg('rect')
+ .attr('width', 36)
+ .attr('height', 36)
+ .attr('x', '-2px')
+ .attr('y', '-2px')
+ .attr('fill', 'red');
+ if (node.data.friends == 'no') {
+ ui.append(border);
+ }
+ ui.append(img);
+ ui.append(svgText);
+ return ui;
+ }
+ }
+ ).placeNode(
+ function(nodeUI, pos){
+ // Shift image to let links go to the center:
+ nodeUI.attr('transform',
+ 'translate(' +
+ (pos.x - 16) + ',' + (pos.y - 16) +
+ ')');
+ }
+ );
+
+ var layout = Viva.Graph.Layout.forceDirected(graph, {
+ stableThreshold : 0.09,
+ springLength : 150,
+ springCoeff : 0.0008,
+ dragCoeff : 0.05,
+ gravity : -1.0
+ });
+
+ var renderer = Viva.Graph.View.renderer(graph, {
+ container: document.getElementById('premades'),
+ graphics : graphics,
+ layout : layout
+ });
+
+ renderer.run();
+ };
+ </script>
+
+ <div id="premades">
+ </div>
+ <div id="info">
+ (Private Profile sind durch eine rote Umrandung gekennzeichnet.)
+ </div>
+ {% endif %}
+
+ </body>
+</html>
+
+{# vim: commentstring={#\ %s\ #}