import argparse import csv import os.path as path import re import constants.fields as F importdir = 'images' datafile = path.join(importdir, 'gps.csv') args = None # Based on imported CSV and added with attributes: # data with appended attributes as list of lists. # Items ---> See constants/fields.py <--- # - ID # - Image file name # - lon # - lat # - title, description # - x, y: Default tile position data = None # Has message in error case error = None # How many tiles on the canvas in (x, y) matrixdims = (6, 3) def parse_args(): parser = argparse.ArgumentParser(description="Site Builder Main Program") parser.add_argument('-i', '--importdir', type=str, help=f'Path to images directory, default={importdir}', default=importdir) parser.add_argument('-d', '--datafile', type=str, help=f'Filename of CSV data file, default={datafile}', default=datafile) parser.add_argument('-v', '--verbose', action='store_true', help='Enable verbose output') parser.add_argument('-vv', '--veryverbose', action='store_true', help='Enable very verbose output') return parser.parse_args() def log(message): if args.verbose or args.veryverbose: print(message) def logvv(message): if args.veryverbose: print(message) def exit_with_error(message): print(message) exit(1) def read_csv_to_dict(csv_file): """ Reads a tab-separated CSV file and returns a list of dictionaries, one per row in the full data item size. Placeholder for unfilled fields is None. Fields: 0: ID (integer), ascending starting with '1' <-- not in CSV 1: image file name (string) 2: latitude and longitude (floats), separated by space. 3. title (string) 4. description (string) The items must be sorted chronologically. Assumes the first row contains headers. In error case, returns None """ global message data = [] with open(csv_file, newline='', encoding='utf-8') as f: reader = csv.reader(f, skipinitialspace=False, delimiter='\t') log(f"Read lines from {csv_file}") id = 0 for row in reader: logvv(f"Processing row: {row}") if not row or len(row) < 4: message = f"Invalid row: {row}" return None if not ' ' in row[1].strip(): message = f'Invalid location: {row[1]}' return None geo = re.findall(r'[\d]*[.][\d]+', row[1]) if len(geo) != 2: message = f'Invalid loc/lon: {row[1]}' id += 1 # Order and size must match constants.fields # values = [id, row[0], float(geo[0]), float(geo[1]), row[2], row[3] ] values = [] values.insert(F.ID, id) values.insert(F.FILE, row[0]) values.insert(F.LON, float(geo[0])) values.insert(F.LAT, float(geo[1])) values.insert(F.TITLE, row[2]) values.insert(F.DESCRIPTION, row[3]) values.insert(F.POSX, None) values.insert(F.POSY, None) data.append(values) logvv(f"Processed item : {values}") if len(data) == 0: message = f'Empty file: {csv_file}' return None else: log(f'Imported {len(data)} records.') return data def read_importdata(): global data if not path.exists(args.importdir): exit_with_error(f"Error: Import directory '{args.importdir}' does not exist.") if not path.exists(args.datafile): exit_with_error(f"Error: Data file '{args.datafile}' does not exist.") log(f"Import directory: {args.importdir}") log(f"Data file: {args.datafile}") data = read_csv_to_dict(args.datafile) if data is None: exit_with_error(message) log("Read successful") def find_geo_extrema(data): """ Find min and max location values for lon and lat. Returns (lonmin, lonmax, latmin, latmax) or None in error case. """ if len(data) == 0: return None lonmin = lonmax = data[0][F.LON] latmin = latmax = data[0][F.LAT] for entry in data[1:]: if entry[F.LON] < lonmin: lonmin = entry[F.LON] if entry[F.LON] > lonmax: lonmax = entry[F.LON] if entry[F.LAT] < latmin: latmin = entry[F.LAT] if entry[F.LAT] > latmax: latmax = entry[F.LAT] return (lonmin, lonmax, latmin, latmax) def translate_pos(min, max, pos, size): """Values must be numbers.""" return round((pos - min) / (max - min) * size) def append_tile_pos(data, geo_extrema, matrixdims): """ Adds two columns with x and y position (integer) of the tile in the canvas """ ret = [] for e in data: x = translate_pos(geo_extrema[2], geo_extrema[3], e[2], matrixdims[0]) y = translate_pos(geo_extrema[0], geo_extrema[1], e[1], matrixdims[1]) e.append(x) e.append(y) ret.append(e) logvv(f'Data updated: {e}') return ret def create_playbook(data): """ Returns list of dict [delta1, delta2, ...] with changes (in both direections) for every single tour. The snapshot holds the complete canvas for the actual step. The playbook contains exclusively changed values (delta). Rules to process: - Loop over all n data items: start with the first (oldest) item (0) - Copy previous snapshot (to find changes in a later step) - Run recursively over the the snapshot items: Find an existing item at the position of the actual image and change its position by the displacement rule. - Place the image of the actual item (a) at its default position in the snapshot - Create a item for the following image (a+1) and hide it (for scrolling backwards). - Store all changes betweeen snapshot and previous snapshot in a new delta (a) and append it to the return dict. In error case, None will be returned. """ snapshot = prev_snapshot = [] ret = [] for a in data: prev_snapshot = snapshot (x, y) = (a[F.POSX], a[F.POSY]) snapshot = shifting_snapshot_items(x,y, snapshot) return ret def shifting_snapshot_items(x, y, snapshot): """ Recursive change item chain starting with item at position x,y. Returns updated snapshot """ goon = True (actx, acty) = (x, y) ret = snapshot while goon: goon = False for i, item in enumerate(ret): if (item[F.POSX], item[F.POSY]) == (actx, acty): # ret[i][F.POSX], ret[i][F.POSY] = goon = True return ret def shifting(tup, matrixdims): """Strategy to shift. Above and on the horizontal middle line, the position will be shifted to the top. Positions below the horizontal middle line, the position will be shifted in bottom direction. Returns the offset as (x, y) tuple, not the new position itself. """ def calculate_orig_pos(): global data geo_extrema = find_geo_extrema(data) data = append_tile_pos(data, geo_extrema, matrixdims) playbook = create_playbook(data) def main(): global args args=parse_args() read_importdata() calculate_orig_pos() log("Finished.") if __name__ == "__main__": main()