Fri, 05 Feb 2021 12:16:29 +0200
update
| 1 | 1 | #!/usr/bin/env python3 |
| 2 | def via_factor(region_name, *, regions): | |
| 3 | # how important is it that | |
| 4 | if region_name in regions: | |
| 5 | return float(regions[region_name]['via_factor']) | |
| 6 | else: | |
| 7 | return 0 | |
| 8 | ||
| 9 | def simplify_name(region_name, *, regions, replace = False): | |
| 10 | # take short_name to account | |
| 11 | region = regions.get(region_name) | |
| 12 | if region: | |
| 13 | return region.get('short_name', region_name) | |
| 14 | else: | |
| 15 | return region_name | |
| 16 | ||
| 17 | def destinations_list( | |
| 18 | itinerary, | |
| 19 | *, trip_length, | |
| 20 | regions, | |
| 21 | whole = False, | |
| 22 | format = 'medium', | |
| 23 | ): | |
| 24 | ''' | |
| 25 | Produces a sign of destinations for the given itinerary. | |
| 26 | `itinerary`: list of region names passed through | |
| 27 | `trip_length`: length of the itinerary in meters. | |
| 28 | `regions`: the regions table, used to decide what is important to show. | |
| 29 | `whole`: whether or not the starting place is also included. | |
| 30 | `format` controls what kind of sign to produce: | |
| 31 | - 'short': at most 2 destinations, with reducing | |
| 32 | - 'medium': at most 3 destinations, with reducing | |
| 33 | - 'long': at most 4 destinations, no reducing. | |
| 34 | Returns a list of region names. | |
| 35 | e.g. ['Turun keskusta', 'Ihala', 'Kauppakeskus Mylly'] | |
| 36 | for Föli bus route 220 at the student village. | |
| 37 | ''' | |
| 38 | # prefer longer destination signs on longer routes | |
| 39 | length = ((trip_length / 600) * 3 + len(itinerary) * 2) / 5 | |
| 40 | # collect regions along the itinerary | |
| 41 | have_already = set() | |
| 42 | i = 0 | |
| 43 | if not itinerary: | |
| 44 | # not going anywhere? | |
| 45 | return '' | |
| 46 | while i < len(itinerary): | |
| 47 | region = regions.get(itinerary[i]) | |
| 48 | if not itinerary[i] or itinerary[i] in have_already: | |
| 49 | del itinerary[i] | |
| 50 | else: | |
| 51 | have_already.add(itinerary[i]) | |
| 52 | i += 1 | |
| 53 | from_place = itinerary[0] | |
| 54 | destination = itinerary[-1] | |
| 55 | route_weights = {} | |
| 56 | # create weights for all places along the way. Transforming by x^-0.3 | |
| 57 | # lessens weights for places further out in the itinerary. | |
| 58 | f = lambda i: i**-0.3 | |
| 59 | # this factor scales the weights so that they become comparable against | |
| 60 | # constant values | |
| 61 | factor = 1 / max(f(i + 1) for i in range(len(itinerary))) | |
| 62 | while via_factor(itinerary[-1], regions = regions) < 0: | |
| 63 | del itinerary[-1] | |
| 64 | if not itinerary: | |
| 65 | return '' | |
| 66 | destination = itinerary[-1] | |
| 67 | for i, stop in enumerate(itinerary): | |
| 68 | # transform index by: | |
| 69 | # - our gradually decreasing x^-0.3 curve, | |
| 70 | # - our normalising factor, | |
| 71 | # - and the via_factor of the stop | |
| 72 | route_weights[stop] = f(i + 1) * factor * via_factor(stop, regions = regions) | |
| 73 | # ensure that the starting region does not make it into the destinations | |
| 74 | # sign by setting its weight to 0 | |
| 75 | if from_place in route_weights: | |
| 76 | route_weights[from_place] = 0 | |
| 77 | # ensure that the destination does make it to the signpost | |
| 78 | route_weights[destination] = 1.0e+10 | |
| 79 | # sort destinations by importance | |
| 80 | weights = sorted( | |
| 81 | [ | |
| 82 | (stop, route_weights[stop], i) | |
| 83 | for i, stop in enumerate(itinerary) | |
| 84 | if route_weights[stop] >= 1 | |
| 85 | ], key = lambda stop: -stop[1]) | |
| 86 | # now consider what do we want to display: | |
| 87 | if format == 'long': | |
| 88 | # long format, just take at most four destinations | |
| 89 | weights = weights[:4] | |
| 90 | elif format == 'short': | |
| 91 | # short format, take at most two destinations | |
| 92 | weights = weights[:2] | |
| 93 | # possibly drop the via-region as well | |
| 94 | try: | |
| 95 | if weights[1][0] != destination and weights[1][1] < (500 / length ** 1.15): | |
| 96 | del weights[1] | |
| 97 | except IndexError: | |
| 98 | pass | |
| 99 | elif format == 'medium': | |
| 100 | # regular format, at most three destinations | |
| 101 | weights = weights[:3] | |
| 102 | # if the third sign value is not significant enough, drop it | |
| 103 | try: | |
| 104 | if weights[2][0] != destination and weights[2][1] < (725 / length ** 0.8): | |
| 105 | del weights[2] | |
| 106 | except IndexError: | |
| 107 | pass | |
| 108 | # and repeat for the second sign value | |
| 109 | try: | |
| 110 | if weights[1][0] != destination and weights[1][1] < (500 / length ** 1.15): | |
| 111 | del weights[1] | |
| 112 | except IndexError: | |
| 113 | pass | |
| 114 | else: | |
| 115 | raise ValueError(str.format('Unknown format {format}', format = repr(format))) | |
| 116 | # restore the signpost back to itinerary order | |
| 117 | weights.sort(key = lambda weight_data: weight_data[2]) | |
| 118 | # form the sign | |
| 119 | sign = [paino[0] for paino in weights] | |
| 120 | old_sign = sign.copy() | |
| 121 | sign = [] | |
| 122 | for place in old_sign: | |
| 123 | if place not in sign: | |
| 124 | sign.append(place) | |
| 125 | if whole: | |
| 126 | # whole format, also include the starting point | |
| 127 | sign = [from_place] + sign | |
| 128 | if not sign: | |
| 129 | sign = [destination] | |
| 130 | return sign |