import argparse import configparser import math import os import sys import ywalk.parser as Parser from ywalk.types import Mode, Place from ywalk.graph import Graph class ParseModeRestriction(argparse.Action): def __call__(self, parser, namespace, args, option_string=None): arglist = set(args.split(',')) for arg in arglist: if arg.upper() not in Mode.__members__: errx(f'Not a valid Mode: {arg}') modes = [Mode[arg.upper()] for arg in arglist] saved = getattr(namespace, self.dest) or [] saved.extend(modes) setattr(namespace, self.dest, saved) # pylint: disable=line-too-long cli = argparse.ArgumentParser() cli.add_argument('place', type=Place, nargs='*', help='a place in Morrowind') cli.add_argument('-w', dest='weight_spec', metavar='WEIGHTING_METHOD', help='the weighting method to apply when searching for a route') cli.add_argument('-P', dest='deny_places', type=Place, action='append', metavar='PLACE', help='avoid traveling through this place') cli.add_argument('-m', dest='allow_modes', action=ParseModeRestriction, metavar='MODES', help='allow only these modes of travel') cli.add_argument('-M', dest='deny_modes', action=ParseModeRestriction, metavar='MODES', help='deny these modes of travel') cli.add_argument('-T', dest='deny_teleport', action='store_true', help='deny teleportation') def errx(msg): sys.exit(f'ywalk: {msg}') def pretty_print_path(path): origin = path[0].origin destination = path[-1].destination time = 0 print(f'Start in {origin}') for conn in path: time = time + conn.time print(f' then {conn.mode.pretty} to {conn.destination}', end='') if conn.time: print(f' ({conn.time} {"hour" if conn.time == 1 else "hours"})') else: print() if time: print(f'Arrive in {destination} after {time} hours.') def pretty_print_place(place, graph): def pretty_print_connections(conns): for conn in sorted(conns, key=lambda c: c.mode.value): print(conn) print(f'Direct connections from {place}:') pretty_print_connections(graph.get_connections_from(place)) print() print(f'Direct connections to {place}:') pretty_print_connections(graph.get_connections_to(place)) print() print ('Reachability map:') weights, _ = graph.shortest_paths(place) for destination in sorted(weights.keys(), key=weights.get): if place == destination: continue reachability = "unreachable" if weights[destination] == math.inf else weights[destination] print(f'{destination}: {reachability}') def get_xdg_home(xdg_type, fallback): env = f'XDG_{xdg_type}_HOME' if env in os.environ: return os.environ[env] return os.path.expanduser(fallback) def apply_predicates(args, graph): if args.allow_modes: graph.add_predicate(lambda c: c.mode in args.allow_modes) if args.deny_modes: graph.add_predicate(lambda c: c.mode not in args.deny_modes) if args.deny_teleport: graph.add_predicate(lambda c: not c.mode.teleport) if args.deny_places: graph.add_predicate(lambda c: c.destination not in args.deny_places) def main(): args = cli.parse_intermixed_args() config = configparser.ConfigParser() config.read(os.path.join(get_xdg_home('CONFIG', '~/.config'), 'ywalk', 'config')) datafile = config.get('misc', 'data', fallback='goty') datapath = os.path.join(get_xdg_home('DATA', '~/.local/share'), 'ywalk', f'{datafile}.tsv') graph = Graph() try: graph.populate(Parser.parse_tsv(datapath)) except (FileNotFoundError, Parser.InputError, ValueError) as err: errx(err) recall = config.get('misc', 'recall', fallback=None) if recall: graph.add_recall(Place(recall)) for place in args.place: if place not in graph: errx(f'Can\'t find "{place}".') graph.set_weight(args.weight_spec or config.get('misc', 'weighting_method', fallback=None)) apply_predicates(args, graph) if not args.place: for place in sorted(graph.get_places(), key=lambda p: p.name): print(place) elif len(args.place) == 1: pretty_print_place(args.place[0], graph) elif len(args.place) > 1: path = graph.find_path(*args.place) if path is None: errx(f'Graph exhausted, no connection between {", ".join(map(str, args.place))}') pretty_print_path(path) if __name__ == '__main__': main()