aboutsummaryrefslogblamecommitdiffstatshomepage
path: root/ywalk/main.py
blob: 3ea414571dfc554fb8d08cef7df408eadf40a210 (plain) (tree)







































































































































                                                                                                                                       
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()