# 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 (
"
"
)
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 {}.
'
#
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)
for line in write_stats(locationsAndDirs, connections, imageDates):
print(f"
{line}
", 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()