# Generate HTML files for Helsinki Virtual Tour. # Image filenames: road_id_1-road_id_2-view_direction.extension # - sort road IDs alphabetically, except "none" is always last import os, sys from collections import Counter from itertools import chain # manually-created text files to read STREETS_FILE = "streets.txt" # IDs and names DATES_FILE = "dates.txt" # IDs and dates (YYYY-MM-DD) CONNECTIONS_FILE = "connections.txt" # between locations and directions # images to read IMAGE_DIRECTORY = "img" # directory IMAGE_EXTENSION = ".jpg" # extension in lower case # files to write SUBPAGE_DIRECTORY = "page" TOC_FILE = "toc.html" # table of contents # names of compass directions DIRECTIONS = { "n": "north", "ne": "northeast", "e": "east", "se": "southeast", "s": "south", "sw": "southwest", "w": "west", "nw": "northwest", } # compass directions in clockwise order DIRECTIONS_ORDER = ("n", "ne", "e", "se", "s", "sw", "w", "nw") # start of subpage HTML HTML_START = """\ Helsinki Virtual Tour
\ """ # end of subpage HTML HTML_END = """\
\ """ # don't count these towards the number of streets IGNORE_STREET_IDS = set(("none", "none2", "none3", "none4", "railw")) # --- Input file reading ------------------------------------------------------ def read_lines(filename): # generate filenames without newlines with open(filename, "rt", encoding="utf8") as handle: handle.seek(0) for line in handle: line = line.rstrip("\n") if line and not line.startswith("#"): yield line def get_filenames(path, extension): # generate filenames having the specified extension (must be in lower # case), without path or the extension with os.scandir(path) as dirIter: for entry in dirIter: (filename, ext) = os.path.splitext(entry.name) if entry.is_file() and ext.lower() == extension: yield filename def get_streets(): # read streets file; generate (id, street) for line in read_lines(STREETS_FILE): parts = line.split("=") if len(parts) != 2: sys.exit("Syntax error: " + line) yield parts def get_image_dates(locationsAndDirs): # generate (street1, street2, direction, date_) tuples; # locationsAndDirs: valid (street1, street2, direction) tuples # for line in read_lines(DATES_FILE): parts = line.split(",") if len(parts) != 2: sys.exit("Syntax error: " + line) (filename, date_) = parts # parts = filename.split("-") if len(parts) != 3: sys.exit("Filename is not valid: " + filename) (street1, street2, direction) = parts # if (street1, street2, direction) not in locationsAndDirs: print( "Warning: nonexistent filename in dates file: {}-{}-{}".format( street1, street2, direction ), file=sys.stderr ) yield (street1, street2, direction, date_) def process_image_filenames(streets): # streets: valid street IDs # generate: (street1, street2, direction) for each image file for filename in get_filenames(IMAGE_DIRECTORY, IMAGE_EXTENSION): parts = filename.split("-") if len(parts) != 3: sys.exit("Image filename not valid: " + filename) (street1, street2, direction) = parts if street1 not in streets: sys.exit("1st street in image filename not valid: " + filename) if street2 not in streets: sys.exit("2nd street in image filename not valid: " + filename) if direction not in DIRECTIONS: sys.exit("Direction in image filename not valid: " + filename) if street1 == street2: sys.exit("Streets are the same in image filename: " + filename) if street1 > street2 and not street2.startswith("none"): sys.exit("Streets out of order in image filename: " + filename) yield (street1, street2, direction) def get_connections(locationsAndDirs): # locationsAndDirs: valid (street1, street2, direction) tuples # generate: connections between streets and locations: # e.g. "street1-street2-n>street3-street4-n" from file # -> (("street1", "street2", "n"), ("street3", "street4", "n")) for line in read_lines(CONNECTIONS_FILE): parts = line.split(">") if len(parts) != 2: sys.exit("Syntax error: " + line) (from_, to_) = (p.strip() for p in parts) # fromParts = tuple(from_.split("-")) if len(fromParts) != 3: sys.exit("Syntax error: " + line) if fromParts not in locationsAndDirs: sys.exit("Connection from nonexistent location/direction.") # toParts = tuple(to_.split("-")) if len(toParts) != 3: sys.exit("Syntax error: " + line) if toParts not in locationsAndDirs: sys.exit("Connection to nonexistent location/direction.") # if fromParts == toParts: sys.exit("Connection from a location/direction to itself.") yield (fromParts, toParts) # --- Output file writing ----------------------------------------------------- def write_toc(streets, locationsAndDirs): # streets: {street_id: street_name, ...} # locationsAndDirs: all (street1, street2, direction) tuples # generate: HTML elements uniqueLocations = list(set( (s1, s2) for (s1, s2, d) in locationsAndDirs )) uniqueLocations.sort(key=lambda p: (streets[p[0]], streets[p[1]])) for (street1, street2) in uniqueLocations: dirs = [ d for (s1, s2, d) in locationsAndDirs if s1 == street1 and s2 == street2 ] dirs.sort(key=lambda d: DIRECTIONS_ORDER.index(d)) links = [] for dir_ in dirs: htmlName = "-".join((street1, street2, dir_)) htmlPath = os.path.join(SUBPAGE_DIRECTORY, htmlName) links.append('{}'.format( htmlPath, DIRECTIONS[dir_] )) yield ( "" + "".join(( streets[street1], streets[street2], ", ".join(links) )) + "" ) def write_stats(locationsAndDirs, connections, imageDates): # locationsAndDirs: all (street1, street2, direction) tuples # connections: a dict; keys and values are (street1, street2, direction) # imageDates: {(street1, street2, direction): date, ...} # generate: contents of each HTML
  • element locations = [(s1, s2) for (s1, s2, d) in locationsAndDirs] # streets = [ s for s in chain.from_iterable(locations) if s not in IGNORE_STREET_IDS ] streetLocCnts = Counter( s for s in chain.from_iterable(set(locations)) if s not in IGNORE_STREET_IDS ).values() locPhotoCnts = Counter(locations).values() streetPhotoCnts = Counter(streets).values() # exitCnts = Counter( (s1, s2) for (s1, s2, d) in connections ).values() entryCnts = Counter( (s1, s2) for (s1, s2, d) in connections.values() ).values() yield "streets: {}".format(len(set(streets))) yield "locations (intersections): {}".format(len(set(locations))) yield "photos: {}".format(len(locationsAndDirs)) yield "connections between locations: {}".format(len(connections)) yield "locations/street: {}–{}".format( min(streetLocCnts), max(streetLocCnts) ) yield "photos/location: {}–{}".format( min(locPhotoCnts), max(locPhotoCnts) ) yield "photos/street: {}–{}".format( min(streetPhotoCnts), max(streetPhotoCnts) ) yield "exits/location: {}–{}".format( min(exitCnts), max(exitCnts) ) yield "entrances/location: {}–{}".format( min(entryCnts), max(entryCnts) ) yield "photos taken on: {}…{}".format( min(imageDates.values()), max(imageDates.values()) ) yield "most photos/day: {}".format( max(Counter(imageDates.values()).values()) ) def get_new_direction(street1, street2, direction, dirOffs, locationsAndDirs): # get direction for left/right turn from current location and direction # if dirOffs = -1: left turn # if dirOffs = 1: right turn # # get all directions available for this location, sorted clockwise allDirs = [ d for (s1, s2, d) in locationsAndDirs if s1 == street1 and s2 == street2 ] if len(allDirs) < 2: sys.exit("Error: less than 2 directions available.") allDirs.sort(key=lambda d: DIRECTIONS_ORDER.index(d)) # return the direction to the left or right from current one newIndex = (allDirs.index(direction) + dirOffs) % len(allDirs) return allDirs[newIndex] def get_navigation_links( street1, street2, direction, locationsAndDirs, connections ): # street1, street2, direction: current location and direction # locationsAndDirs: all (street1, street2, direction) tuples # connections: a dict; keys and values are (street1, street2, direction) # return: a list of navigation links links = [] links.append('Turn left'.format( street1, street2, get_new_direction( street1, street2, direction, -1, locationsAndDirs ), )) links.append('Turn right'.format( street1, street2, get_new_direction( street1, street2, direction, 1, locationsAndDirs ), )) fwdDest = connections.get((street1, street2, direction)) if fwdDest is None: links.append("you cannot go forward") else: links.append('Go forward'.format(*fwdDest)) return links def write_subpage( street1, street2, direction, streets, locationsAndDirs, connections, imageDates ): # street1, street2, direction: current location and direction # streets: {street_id: street_name, ...} # locationsAndDirs: all (street1, street2, direction) tuples # connections: a dict; keys and values are (street1, street2, direction) # imageDates: {(street1, street2, direction): date, ...} # generate: lines to write to a subpage (one image each) yield HTML_START # description yield ( "

    You stand at the intersection of {} and {}, " "looking {}, on {}.

    ".format( streets[street1], streets[street2], DIRECTIONS[direction], imageDates[(street1, street2, direction)] ) ) # navigation links links = get_navigation_links( street1, street2, direction, locationsAndDirs, connections ) yield "

    " + " | ".join(links) + "

    " # photo imgFilename = "-".join((street1, street2, direction)) + IMAGE_EXTENSION imgPath = os.path.join("..", IMAGE_DIRECTORY, imgFilename) yield f'

    street photo

    ' # yield HTML_END # ----------------------------------------------------------------------------- def main(): print("Reading street IDs and names...") streets = {} # {street_id: street_name, ...} for (id_, street) in get_streets(): if id_ in streets: sys.exit("Duplicate street id: " + id_) streets[id_] = street # (street1, street2, direction) tuples print("Reading image files...") locationsAndDirs = frozenset(process_image_filenames(streets.keys())) print("Reading dates of images...") imageDates = {} for (street1, street2, direction, date_) in get_image_dates( locationsAndDirs ): imageDates[(street1, street2, direction)] = date_ for (street1, street2, direction) in locationsAndDirs: if (street1, street2, direction) not in imageDates: sys.exit("Error: no date for image: {}-{}-{}".format( street1, street2, direction )) print("Reading connections between locations and directions...") connections = {} for (source, dest) in get_connections(locationsAndDirs): connections[source] = dest for pos in ( set((s1, s2) for (s1, s2, d) in locationsAndDirs) - set((s1, s2) for (s1, s2, d) in connections.values()) ): print( f"Warning: intersection of '{pos[0]}' and '{pos[1]}' is " "unreachable from any other location.", file=sys.stderr ) print( f"Writing table of contents and statistics to {TOC_FILE}... (copy it " "to index.html manually)" ) with open(TOC_FILE, "wt", encoding="utf8") as handle: handle.seek(0) for line in write_toc(streets, locationsAndDirs): print(" " + line, file=handle) print("", file=handle) # print('

    Statistics

    ', file=handle) print("", file=handle) print("Writing subpages...") for (street1, street2, direction) in locationsAndDirs: htmlFilename = "-".join((street1, street2, direction)) + ".html" htmlPath = os.path.join(SUBPAGE_DIRECTORY, htmlFilename) with open(htmlPath, "wt", encoding="utf8") as handle: handle.seek(0) for line in write_subpage( street1, street2, direction, streets, locationsAndDirs, connections, imageDates ): print(line, file=handle) main()