]> de.git.xonotic.org Git - xonotic/xonstat.git/blob - xonstat/util.py
Merge zykure-approved, my fixes, and merge fixes :D
[xonotic/xonstat.git] / xonstat / util.py
1 import re
2 from colorsys import rgb_to_hls, hls_to_rgb
3 from cgi import escape as html_escape
4 from datetime import datetime, timedelta
5 from decimal import Decimal
6 from collections import namedtuple
7
8 # Map of special chars to ascii from Darkplace's console.c.
9 _qfont_table = [
10  '\0', '#',  '#',  '#',  '#',  '.',  '#',  '#',
11  '#',  '\t', '\n', '#',  ' ',  '\r', '.',  '.',
12  '[',  ']',  '0',  '1',  '2',  '3',  '4',  '5',
13  '6',  '7',  '8',  '9',  '.',  '<',  '=',  '>',
14  ' ',  '!',  '"',  '#',  '$',  '%',  '&',  '\'',
15  '(',  ')',  '*',  '+',  ',',  '-',  '.',  '/',
16  '0',  '1',  '2',  '3',  '4',  '5',  '6',  '7',
17  '8',  '9',  ':',  ';',  '<',  '=',  '>',  '?',
18  '@',  'A',  'B',  'C',  'D',  'E',  'F',  'G',
19  'H',  'I',  'J',  'K',  'L',  'M',  'N',  'O',
20  'P',  'Q',  'R',  'S',  'T',  'U',  'V',  'W',
21  'X',  'Y',  'Z',  '[',  '\\', ']',  '^',  '_',
22  '`',  'a',  'b',  'c',  'd',  'e',  'f',  'g',
23  'h',  'i',  'j',  'k',  'l',  'm',  'n',  'o',
24  'p',  'q',  'r',  's',  't',  'u',  'v',  'w',
25  'x',  'y',  'z',  '{',  '|',  '}',  '~',  '<',
26
27  '<',  '=',  '>',  '#',  '#',  '.',  '#',  '#',
28  '#',  '#',  ' ',  '#',  ' ',  '>',  '.',  '.',
29  '[',  ']',  '0',  '1',  '2',  '3',  '4',  '5',
30  '6',  '7',  '8',  '9',  '.',  '<',  '=',  '>',
31  ' ',  '!',  '"',  '#',  '$',  '%',  '&',  '\'',
32  '(',  ')',  '*',  '+',  ',',  '-',  '.',  '/',
33  '0',  '1',  '2',  '3',  '4',  '5',  '6',  '7',
34  '8',  '9',  ':',  ';',  '<',  '=',  '>',  '?',
35  '@',  'A',  'B',  'C',  'D',  'E',  'F',  'G',
36  'H',  'I',  'J',  'K',  'L',  'M',  'N',  'O',
37  'P',  'Q',  'R',  'S',  'T',  'U',  'V',  'W',
38  'X',  'Y',  'Z',  '[',  '\\', ']',  '^',  '_',
39  '`',  'a',  'b',  'c',  'd',  'e',  'f',  'g',
40  'h',  'i',  'j',  'k',  'l',  'm',  'n',  'o',
41  'p',  'q',  'r',  's',  't',  'u',  'v',  'w',
42  'x',  'y',  'z',  '{',  '|',  '}',  '~',  '<'
43 ]
44
45 # Hex-colored spans for decimal color codes ^0 - ^9
46 _dec_spans = [
47  "<span style='color:rgb(128,128,128)'>",
48  "<span style='color:rgb(255,0,0)'>",
49  "<span style='color:rgb(51,255,0)'>",
50  "<span style='color:rgb(255,255,0)'>",
51  "<span style='color:rgb(51,102,255)'>",
52  "<span style='color:rgb(51,255,255)'>",
53  "<span style='color:rgb(255,51,102)'>",
54  "<span style='color:rgb(255,255,255)'>",
55  "<span style='color:rgb(153,153,153)'>",
56  "<span style='color:rgb(128,128,128)'>"
57 ]
58
59 # Color code patterns
60 _all_colors = re.compile(r'\^(\d|x[\dA-Fa-f]{3})')
61 _dec_colors = re.compile(r'\^(\d)')
62 _hex_colors = re.compile(r'\^x([\dA-Fa-f])([\dA-Fa-f])([\dA-Fa-f])')
63
64 # On a light scale of 0 (black) to 1.0 (white)
65 _contrast_threshold = 0.5
66
67
68 def qfont_decode(qstr=''):
69     """ Convert the qfont characters in a string to ascii.
70     """
71     if qstr == None:
72         qstr = ''
73     chars = []
74     for c in qstr:
75         if u'\ue000' <= c <= u'\ue0ff':
76             c = _qfont_table[ord(c) - 0xe000]
77         chars.append(c)
78     return ''.join(chars)
79
80
81 def strip_colors(qstr=''):
82     if qstr == None:
83         qstr = ''
84     return _all_colors.sub('', qstr)
85
86
87 def hex_repl(match):
88     """Convert Darkplaces hex color codes to CSS rgb.
89     Brighten colors with HSL light value less than 50%"""
90
91     # Extend hex char to 8 bits and to 0.0-1.0 scale
92     r = int(match.group(1) * 2, 16) / 255.0
93     g = int(match.group(2) * 2, 16) / 255.0
94     b = int(match.group(3) * 2, 16) / 255.0
95
96     # Check if color is too dark
97     hue, light, satur = rgb_to_hls(r, g, b)
98     if light < _contrast_threshold:
99         light = _contrast_threshold
100         r, g, b = hls_to_rgb(hue, light, satur)
101
102     # Convert back to 0-255 scale for css
103     return '<span style="color:rgb(%d,%d,%d)">' % (255 * r, 255 * g, 255 * b)
104
105
106 def html_colors(qstr='', limit=None):
107     qstr = html_escape(qfont_decode(qstr))
108     qstr = qstr.replace('^^', '^')
109
110     if limit is not None and limit > 0:
111         qstr = limit_printable_characters(qstr, limit)
112
113     html = _dec_colors.sub(lambda match: _dec_spans[int(match.group(1))], qstr)
114     html = _hex_colors.sub(hex_repl, html)
115     return html + "</span>" * len(_all_colors.findall(qstr))
116
117
118 def limit_printable_characters(qstr, limit):
119     # initialize assuming all printable characters
120     pc = [1 for i in range(len(qstr))]
121
122     groups = _all_colors.finditer(qstr)
123     for g in groups:
124         pc[g.start():g.end()] = [0 for i in range(g.end() - g.start())]
125
126     # printable characters in the string is less than or equal to what was requested
127     if limit >= len(qstr) or sum(pc) <= limit:
128         return qstr
129     else:
130         sumpc = 0
131         for i,v in enumerate(pc):
132             sumpc += v
133             if sumpc == limit:
134                 return qstr[0:i+1]
135
136
137 def page_url(page):
138     return current_route_url(request, page=page, _query=request.GET)
139
140
141 def pretty_date(time=False):
142     '''Returns a human-readable relative date.'''
143     now = datetime.utcnow()
144     if type(time) is int:
145         diff = now - datetime.fromtimestamp(time)
146     elif isinstance(time,datetime):
147         diff = now - time
148     elif not time:
149         print "not a time value"
150         diff = now - now
151
152     dim = round(diff.seconds/60.0 + diff.days*1440.0)
153
154     if dim == 0:
155         return "less than a minute ago"
156     elif dim == 1:
157         return "1 minute ago"
158     elif dim >= 2 and dim <= 44:
159         return "{0} minutes ago".format(int(dim))
160     elif dim >= 45 and dim <= 89:
161         return "about 1 hour ago"
162     elif dim >= 90 and dim <= 1439:
163         return "about {0} hours ago".format(int(round(dim/60.0)))
164     elif dim >= 1440 and dim <= 2519:
165         return "1 day ago"
166     elif dim >= 2520 and dim <= 43199:
167         return "{0} days ago".format(int(round(dim/1440.0)))
168     elif dim >= 43200 and dim <= 86399:
169         return "about 1 month ago"
170     elif dim >= 86400 and dim <= 525599:
171         return "{0} months ago".format(int(round(dim/43200.0)))
172     elif dim >= 525600 and dim <= 655199:
173         return "about 1 year ago"
174     elif dim >= 655200 and dim <= 914399:
175         return "over 1 year ago"
176     elif dim >= 914400 and dim <= 1051199:
177         return "almost 2 years ago"
178     else:
179         return "about {0} years ago".format(int(round(dim/525600.0)))
180
181 def datetime_seconds(td):
182     return float(td.microseconds + (td.seconds + td.days * 24 * 3600) * 10**6) / 10**6
183
184 def to_json(data):
185     if not type(data) == dict:
186         # assume it's a named tuple
187         data = data._asdict()
188     result = {}
189     for key,value in data.items():
190         if value == None:
191             result[key] = None
192         elif type(value) in [bool,int,long,float,complex,str]:
193             result[key] = value
194         elif type(value) == unicode:
195             result[key] = value.encode('utf-8')
196         elif type(value) == Decimal:
197             result[key] = float(value)
198         elif type(value) == datetime:
199             result[key] = value.strftime('%Y-%m-%dT%H:%M:%SZ')
200         elif type(value) == timedelta:
201             result[key] = datetime_seconds(value)
202         else:
203             result[key] = to_json(value.to_dict())
204     return result
205