diff --git a/EDMarketConnector.py b/EDMarketConnector.py index 8ef1778e..27de0eb1 100755 --- a/EDMarketConnector.py +++ b/EDMarketConnector.py @@ -8,6 +8,8 @@ from os.path import expanduser, isdir, join import re import requests from time import time, localtime, strftime +import urllib +import webbrowser import Tkinter as tk import ttk @@ -21,16 +23,53 @@ import companion import bpc import td import eddn +import edsm import loadout import coriolis import flightlog +import eddb import prefs from config import appname, applongname, config from hotkey import hotkeymgr l10n.Translations().install() +EDDB = eddb.EDDB() SHIPYARD_RETRY = 5 # retry pause for shipyard data [s] +EDSM_POLL = 0.1 + + +class HyperlinkLabel(ttk.Label): + + def __init__(self, master=None, **kw): + self.urlfn = kw.pop('urlfn', None) + ttk.Label.__init__(self, master, **kw) + self.font_n = kw.get('font', ttk.Style().lookup('TLabel', 'font')) + self.font_u = tkFont.Font(self, self.font_n) + self.font_u.configure(underline = True) + self.bind('', self._enter) + self.bind('', self._leave) + self.bind('', self._click) + + # Make blue and clickable if setting non-empty text + def __setitem__(self, key, value): + if key=='text': + if self.urlfn and value: + self.configure({key: value}, foreground = 'blue', cursor = platform=='darwin' and 'pointinghand' or 'hand2') + else: + self.configure({key: value}, foreground = '', cursor = 'arrow') + else: + self.configure({key: value}) + + def _enter(self, event): + self.configure(font = self.font_u) + + def _leave(self, event): + self.configure(font = self.font_n) + + def _click(self, event): + if self.urlfn and self['text']: + webbrowser.open(self.urlfn(self['text'])) class AppWindow: @@ -39,6 +78,7 @@ class AppWindow: self.holdofftime = config.getint('querytime') + companion.holdoff self.session = companion.Session() + self.edsm = edsm.EDSM() self.w = master self.w.title(applongname) @@ -73,9 +113,9 @@ class AppWindow: ttk.Label(frame, text=_('System:')).grid(row=1, column=0, sticky=tk.W) # Main window ttk.Label(frame, text=_('Station:')).grid(row=2, column=0, sticky=tk.W) # Main window - self.cmdr = ttk.Label(frame, width=-20) - self.system = ttk.Label(frame, width=-20) - self.station = ttk.Label(frame, width=-20) + self.cmdr = ttk.Label(frame, width=-21) + self.system = HyperlinkLabel(frame, compound=tk.RIGHT, urlfn = self.system_url) + self.station = HyperlinkLabel(frame, urlfn = self.station_url) self.button = ttk.Button(frame, text=_('Update'), command=self.getandsend, default=tk.ACTIVE, state=tk.DISABLED) # Update button in main window self.status = ttk.Label(frame, width=-25) self.w.bind('', self.getandsend) @@ -211,7 +251,8 @@ class AppWindow: self.cmdr['text'] = data.get('commander') and data.get('commander').get('name') or '' self.system['text'] = data.get('lastSystem') and data.get('lastSystem').get('name') or '' - self.station['text'] = data.get('commander') and data.get('commander').get('docked') and data.get('lastStarport') and data.get('lastStarport').get('name') or '-' + self.system['image'] = None + self.station['text'] = data.get('commander') and data.get('commander').get('docked') and data.get('lastStarport') and data.get('lastStarport').get('name') or (EDDB.system(self.system['text'] and '-' or '')) config.set('querytime', querytime) self.holdofftime = querytime + companion.holdoff @@ -229,9 +270,14 @@ class AppWindow: elif (config.getint('output') & config.OUT_EDDN) and data['commander'].get('docked') and not data['lastStarport'].get('ships') and not retrying: # API is flakey about shipyard info - retry if missing (<1s is usually sufficient - 5s for margin). - self.w.after(SHIPYARD_RETRY * 1000, lambda:self.getandsend(event, retrying=True)) + self.w.after(int(SHIPYARD_RETRY * 1000), lambda:self.getandsend(event, retrying=True)) # Stuff we can do while waiting for retry + + self.edsm.start_lookup(self.system['text']) + self.system['image'] = self.edsm.result['img'] + self.w.after(int(EDSM_POLL * 1000), self.edsmpoll) + if config.getint('output') & config.OUT_LOG: flightlog.export(data) if config.getint('output') & config.OUT_SHIP_EDS: @@ -249,6 +295,10 @@ class AppWindow: h.write(json.dumps(data, indent=2, sort_keys=True)) if not retrying: + self.edsm.start_lookup(self.system['text']) + self.system['image'] = self.edsm.result['img'] + self.w.after(int(EDSM_POLL * 1000), self.edsmpoll) + if config.getint('output') & config.OUT_LOG: flightlog.export(data) if config.getint('output') & config.OUT_SHIP_EDS: @@ -318,6 +368,28 @@ class AppWindow: self.cooldown() + def edsmpoll(self): + result = self.edsm.result + if result['done']: + self.system['image'] = result['img'] + else: + self.w.after(int(EDSM_POLL * 1000), self.edsmpoll) + + def system_url(self, text): + return text and self.edsm.result['url'] + + def station_url(self, text): + if text: + station_id = EDDB.station(self.system['text'], self.station['text']) + if station_id: + return 'http://eddb.io/station/%d' % station_id + + system_id = EDDB.system(self.system['text']) + if system_id: + return 'http://eddb.io/system/%d' % system_id + + return None + def cooldown(self): if time() < self.holdofftime: self.button['text'] = _('cooldown {SS}s').format(SS = int(self.holdofftime - time())) # Update button in main window diff --git a/EDMarketConnector.wxs b/EDMarketConnector.wxs index 8405691b..abeaae38 100644 --- a/EDMarketConnector.wxs +++ b/EDMarketConnector.wxs @@ -103,6 +103,12 @@ + + + + + + @@ -355,6 +361,8 @@ + + diff --git a/README.md b/README.md index fdfca336..80f32a03 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,11 @@ This app downloads commodity market and other data from the game [Elite: Dangero The user-interface is deliberately minimal - when you land at a station just switch to the app and press the “Update” button or press Enter to automatically download and transmit and/or save your choice of data. -![Windows screenshot](img/win.png) ![Mac screenshot](img/mac.png) +Click on the system name to go to its [Elite: Dangerous Star Map](http://www.edsm.net/) (“EDSM”) entry in your web broswer. + +Click on the station name to go to its [Elite: Dangerous Database](http://eddb.io/) (“eddb”) entry in your web broswer. + +![Windows screenshot](img/win.png)   ![Mac screenshot](img/mac.png) Installation diff --git a/companion.py b/companion.py index 342b41f1..e18a5714 100644 --- a/companion.py +++ b/companion.py @@ -1,7 +1,6 @@ #!/usr/bin/python # -*- coding: utf-8 -*- -import json import requests from collections import defaultdict from cookielib import LWPCookieJar @@ -197,7 +196,7 @@ class Session: r.raise_for_status() try: - data = json.loads(r.text) + data = r.json() except: self.dump(r) raise ServerError() diff --git a/eddb.py b/eddb.py new file mode 100755 index 00000000..22168865 --- /dev/null +++ b/eddb.py @@ -0,0 +1,53 @@ +#!/usr/bin/python +# +# eddb.io station database +# + +import cPickle +from os.path import dirname, join, normpath +import sys +from sys import platform + +class EDDB: + + def __init__(self): + self.system_ids = cPickle.load(open(join(self.respath(), 'systems.p'), 'rb')) + self.station_ids = cPickle.load(open(join(self.respath(), 'stations.p'), 'rb')) + + # system_name -> system_id + def system(self, system_name): + return self.system_ids.get(system_name, 0) # return 0 on failure (0 is not a valid id) + + # (system_name, station_name) -> station_id + def station(self, system_name, station_name): + return self.station_ids.get((self.system_ids.get(system_name), station_name), 0) # return 0 on failure (0 is not a valid id) + + def respath(self): + if getattr(sys, 'frozen', False): + if platform=='darwin': + return normpath(join(dirname(sys.executable), os.pardir, 'Resources')) + else: + return dirname(sys.executable) + elif __file__: + return dirname(__file__) + else: + return '.' + + +# build system & station database from files systems.json and stations_lite.json from http://eddb.io/api +if __name__ == "__main__": + import json + + # system_name by system_id + systems = dict([(x['id'], str(x['name'])) for x in json.loads(open('systems.json').read())]) + + stations = json.loads(open('stations_lite.json').read()) + + # system_id by system_name - populated systems only + system_ids = dict([(systems[x['system_id']], x['system_id']) for x in stations]) + cPickle.dump(system_ids, open('systems.p', 'wb'), protocol = cPickle.HIGHEST_PROTOCOL) + + # station_id by (system_id, station_name) + station_ids = dict([((x['system_id'], str(x['name'])), x['id']) for x in stations]) + cPickle.dump(station_ids, open('stations.p', 'wb'), protocol = cPickle.HIGHEST_PROTOCOL) + diff --git a/edsm.py b/edsm.py new file mode 100644 index 00000000..d3db578f --- /dev/null +++ b/edsm.py @@ -0,0 +1,63 @@ +import requests +import threading +from sys import platform +import urllib + +import Tkinter as tk + +if __debug__: + from traceback import print_exc + +class EDSM: + + _TIMEOUT = 10 + + def __init__(self): + self.result = { 'img': None, 'url': None, 'done': True } + EDSM._IMG_WAIT_MAC = tk.PhotoImage(data = 'R0lGODlhDgAQAKEBAAAAAP///////////yH5BAEKAAIALAAAAAAOABAAAAIrlAWpx6jZzoPRvQqC3qBlzjGfNnbSFpQmQibcOqKpKIe0vIpTZS3Y/rscCgA7') # wristwatch + EDSM._IMG_WAIT_WIN = tk.PhotoImage(data = 'R0lGODlhDgAQAKEBAAAAAP///////////yH5BAEKAAIALAAAAAAOABAAAAIuFI4JwurcgpxhQUOnhUD2Xl1R5YmcZl5fqoYsVqYgKs527ZHu+ZGb4UhwgghGAQA7') # hourglass + EDSM._IMG_KNOWN = tk.PhotoImage(data = 'R0lGODlhDgAOAMIEAFWjVVWkVWS/ZGfFZwAAAAAAAAAAAAAAACH5BAEKAAQALAAAAAAOAA4AAAMsSLrcHEIEp8C4GDSLu15dOCyB2E2EYGKCoq5DS5QwSsDjwomfzlOziA0ITAAAOw==') # green circle + EDSM._IMG_UNKNOWN = tk.PhotoImage(data = 'R0lGODlhDgAOAMIEAM16BM57BfCPBfiUBgAAAAAAAAAAAAAAACH5BAEKAAQALAAAAAAOAA4AAAMsSLrcHEIEp8C4GDSLu15dOCyB2E2EYGKCoq5DS5QwSsDjwomfzlOziA0ITAAAOw==') # orange circle + EDSM._IMG_NOTFOUND = tk.PhotoImage(data = 'R0lGODlhDgAOAKECAGVLJ+ddWO5fW+5fWyH5BAEKAAMALAAAAAAOAA4AAAImnI+JEAFqgJj0LYqFNTkf2VVGEFLBWE7nAJZbKlzhFnX00twQVAAAOw==') # red circle + EDSM._IMG_NEW = tk.PhotoImage(data = 'R0lGODlhEAAQAMZwANKVHtWcIteiHuiqLPCuHOS1MN22ZeW7ROG6Zuu9MOy+K/i8Kf/DAuvCVf/FAP3BNf/JCf/KAPHHSv7ESObHdv/MBv/GRv/LGP/QBPXOPvjPQfjQSvbRSP/UGPLSae7Sfv/YNvLXgPbZhP7dU//iI//mAP/jH//kFv7fU//fV//ebv/iTf/iUv/kTf/iZ/vgiP/hc/vgjv/jbfriiPriiv7ka//if//jd//sJP/oT//tHv/mZv/sLf/rRP/oYv/rUv/paP/mhv/sS//oc//lkf/mif/sUf/uPv/qcv/uTv/uUv/vUP/qhP/xP//pm//ua//sf//ubf/wXv/thv/tif/slv/tjf/smf/yYP/ulf/2R//2Sv/xkP/2av/0gP/ylf/2df/0i//0j//0lP/5cP/7a//1p//5gf/7ev/3o//2sf/5mP/6kv/2vP/3y//+jP///////////////////////////////////////////////////////////////yH5BAEKAH8ALAAAAAAQABAAAAePgH+Cg4SFhoJKPIeHYT+LhVppUTiPg2hrUkKPXWdlb2xHJk9jXoNJQDk9TVtkYCUkOy4wNjdGfy1UXGJYOksnPiwgFwwYg0NubWpmX1ArHREOFYUyWVNIVkxXQSoQhyMoNVUpRU5EixkcMzQaGy8xhwsKHiEfBQkSIg+GBAcUCIIBBDSYYGiAAUMALFR6FAgAOw==') + EDSM._IMG_ERROR = tk.PhotoImage(data = 'R0lGODlhDgAOAIABAAAAAP///yH5BAEKAAEALAAAAAAOAA4AAAIcjAOpx+rAUGrzVHujWRrDvmWdOH5geKZqSmpkAQA7') # BBC Mode 7 '?' + + def start_lookup(self, system_name): + self.cancel_lookup() + self.result = { 'img': None, 'url': 'http://www.edsm.net/needed-distances?systemName=%s' % urllib.quote(system_name), 'done': False } # default URL + self.thread = threading.Thread(target = self.worker, name = 'EDSM worker', args = (system_name, self.result)) + self.thread.daemon = True + self.thread.start() + + def cancel_lookup(self): + self.thread = None # orphan any existing thread + self.result = { 'img': None, 'url': None, 'done': True } # orphan existing thread's results + + def worker(self, system_name, result): + try: + r = requests.get('http://www.edsm.net/api-v1/system?sysname=%s&coords=1' % urllib.quote(system_name), timeout=EDSM._TIMEOUT) + r.raise_for_status() + data = r.json() + + if data == -1: + # System not present - create it + result['img'] = EDSM._IMG_NEW + result['done'] = True # give feedback immediately + requests.get('http://www.edsm.net/api-v1/url?sysname=%s' % urllib.quote(system_name), timeout=EDSM._TIMEOUT) # creates system + elif data.get('coords'): + # Prefer to send user to "Show distances" page for systems with known coordinates + result['img'] = EDSM._IMG_KNOWN + result['done'] = True # give feedback immediately + try: + r = requests.get('http://www.edsm.net/api-v1/url?sysname=%s' % urllib.quote(system_name), timeout=EDSM._TIMEOUT) + r.raise_for_status() + data = r.json() + result['url'] = data['url']['show-system'].replace('\\','') + except: + if __debug__: print_exc() + else: + result['img'] = EDSM._IMG_UNKNOWN + except: + if __debug__: print_exc() + result['img'] = EDSM._IMG_ERROR + result['done'] = True diff --git a/img/mac.png b/img/mac.png index 73f3f429..69f8622b 100644 Binary files a/img/mac.png and b/img/mac.png differ diff --git a/img/win.png b/img/win.png index e84b3504..952c6ad1 100644 Binary files a/img/win.png and b/img/win.png differ diff --git a/setup.py b/setup.py index 90cae6ff..db51d399 100755 --- a/setup.py +++ b/setup.py @@ -65,6 +65,7 @@ if sys.platform=='darwin': 'excludes': [ 'PIL', 'simplejson' ], 'iconfile': '%s.icns' % APPNAME, 'resources': ['snd_good.wav', 'snd_bad.wav'], + 'resources': ['stations.p', 'systems.p'], 'semi_standalone': True, 'site_packages': False, 'plist': { @@ -101,6 +102,8 @@ elif sys.platform=='win32': 'WinSparkle.pdb', # For debugging - don't include in package 'snd_good.wav', 'snd_bad.wav', + 'stations.p', + 'systems.p', '%s.VisualElementsManifest.xml' % APPNAME, '%s.ico' % APPNAME ] + [join('L10n',x) for x in os.listdir('L10n') if x.endswith('.strings')] ) ] diff --git a/stations.p b/stations.p new file mode 100644 index 00000000..d7aa4df0 Binary files /dev/null and b/stations.p differ diff --git a/systems.p b/systems.p new file mode 100644 index 00000000..5d483bf5 Binary files /dev/null and b/systems.p differ