aboutsummaryrefslogtreecommitdiffstatshomepage
path: root/ywalk/main.py
blob: 3ea414571dfc554fb8d08cef7df408eadf40a210 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
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()