busroute.py

changeset 1
f9788970fa46
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/busroute.py	Wed Jul 29 23:45:53 2020 +0300
@@ -0,0 +1,130 @@
+#!/usr/bin/env python3
+def via_factor(region_name, *, regions):
+	# how important is it that 
+	if region_name in regions:
+		return float(regions[region_name]['via_factor'])
+	else:
+		return 0
+
+def simplify_name(region_name, *, regions, replace = False):
+	# take short_name to account
+	region = regions.get(region_name)
+	if region:
+		return region.get('short_name', region_name)
+	else:
+		return region_name
+
+def destinations_list(
+	itinerary,
+	*, trip_length,
+	regions,
+	whole = False,
+	format = 'medium',
+):
+	'''
+		Produces a sign of destinations for the given itinerary.
+		`itinerary`: list of region names passed through
+		`trip_length`: length of the itinerary in meters.
+		`regions`: the regions table, used to decide what is important to show.
+		`whole`: whether or not the starting place is also included.
+		`format` controls what kind of sign to produce:
+			- 'short': at most 2 destinations, with reducing
+			- 'medium': at most 3 destinations, with reducing
+			- 'long': at most 4 destinations, no reducing.
+		Returns a list of region names.
+		e.g. ['Turun keskusta', 'Ihala', 'Kauppakeskus Mylly']
+		for Föli bus route 220 at the student village.
+	'''
+	# prefer longer destination signs on longer routes
+	length = ((trip_length / 600) * 3 + len(itinerary) * 2) / 5
+	# collect regions along the itinerary
+	have_already = set()
+	i = 0
+	if not itinerary:
+		# not going anywhere?
+		return ''
+	while i < len(itinerary):
+		region = regions.get(itinerary[i])
+		if not itinerary[i] or itinerary[i] in have_already:
+			del itinerary[i]
+		else:
+			have_already.add(itinerary[i])
+			i += 1
+	from_place = itinerary[0]
+	destination = itinerary[-1]
+	route_weights = {}
+	# create weights for all places along the way. Transforming by x^-0.3
+	# lessens weights for places further out in the itinerary.
+	f = lambda i: i**-0.3
+	# this factor scales the weights so that they become comparable against
+	# constant values
+	factor = 1 / max(f(i + 1) for i in range(len(itinerary)))
+	while via_factor(itinerary[-1], regions = regions) < 0:
+		del itinerary[-1]
+		if not itinerary:
+			return ''
+		destination = itinerary[-1]
+	for i, stop in enumerate(itinerary):
+		# transform index by:
+		# - our gradually decreasing x^-0.3 curve,
+		# - our normalising factor,
+		# - and the via_factor of the stop
+		route_weights[stop] = f(i + 1) * factor * via_factor(stop, regions = regions)
+	# ensure that the starting region does not make it into the destinations
+	# sign by setting its weight to 0
+	if from_place in route_weights:
+		route_weights[from_place] = 0
+	# ensure that the destination does make it to the signpost
+	route_weights[destination] = 1.0e+10
+	# sort destinations by importance
+	weights = sorted(
+		[
+			(stop, route_weights[stop], i)
+			for i, stop in enumerate(itinerary)
+			if route_weights[stop] >= 1
+		], key = lambda stop: -stop[1])
+	# now consider what do we want to display:
+	if format == 'long':
+		# long format, just take at most four destinations
+		weights = weights[:4]
+	elif format == 'short':
+		# short format, take at most two destinations
+		weights = weights[:2]
+		# possibly drop the via-region as well
+		try:
+			if weights[1][0] != destination and weights[1][1] < (500 / length ** 1.15):
+				del weights[1]
+		except IndexError:
+			pass
+	elif format == 'medium':
+		# regular format, at most three destinations
+		weights = weights[:3]
+		# if the third sign value is not significant enough, drop it
+		try:
+			if weights[2][0] != destination and weights[2][1] < (725 / length ** 0.8):
+				del weights[2]
+		except IndexError:
+			pass
+		# and repeat for the second sign value
+		try:
+			if weights[1][0] != destination and weights[1][1] < (500 / length ** 1.15):
+				del weights[1]
+		except IndexError:
+			pass
+	else:
+		raise ValueError(str.format('Unknown format {format}', format = repr(format)))
+	# restore the signpost back to itinerary order
+	weights.sort(key = lambda weight_data: weight_data[2])
+	# form the sign
+	sign = [paino[0] for paino in weights]
+	old_sign = sign.copy()
+	sign = []
+	for place in old_sign:
+		if place not in sign:
+			sign.append(place)
+	if whole:
+		# whole format, also include the starting point
+		sign = [from_place] + sign
+	if not sign:
+		sign = [destination]
+	return sign

mercurial