import argparse import csv import os.path as path import re import constants.datafields as F import constants.pos as P importdir = 'images' datafile = path.join(importdir, 'gps.csv') args = None # ---------- # Image data # ---------- # Based on imported CSV and added with attributes: # data with appended attributes as list of lists. # n: Number of images # Items ---> See constants/fields.py <--- # - ID: Image ID [0..n-1] # - Image file name # - lon # - lat # - title # - description # - pos as (x, y): Default tile position data = None # Default tile positions. Key is the image ID and value is the position as (x, y) # Example: { '1': (10,11), '2': (20,22), .... } defaultpos = None # ---------- # Playbook # ---------- # List with changes for every single screen. # Playbook item: # - Screen number [0..n-1] # - list of changes: # - Image ID # - pos as (x, y) or, if to hide, None playbook = [] # 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.GEO, (float(geo[0]), float(geo[1]))) values.insert(F.TITLE, row[2]) values.insert(F.DESCRIPTION, row[3]) values.insert(F.POS, 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(): data = None 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") return data 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.GEO][P.LON] latmin = latmax = data[0][F.GEO][P.LAT] for entry in data[1:]: if entry[F.GEO][P.LON] < lonmin: lonmin = entry[F.GEO][P.LON] if entry[F.GEO][P.LON] > lonmax: lonmax = entry[F.GEO][P.LON] if entry[F.GEO][P.LAT] < latmin: latmin = entry[F.GEO][P.LAT] if entry[F.GEO][P.LAT] > latmax: latmax = entry[F.GEO][P.LAT] ret = (latmin, latmax, lonmin, lonmax) log(f'find_geo_extrema={ret}') return ret def translate_pos(min, max, pos, size): """Values must be numbers.""" return round((pos - min) / (max - min) * size) def append_tile_pos_DEPTRECATED(data, geo_extrema, matrixdims): """ Adds one column with (x, y) position (integers) of the tile in the canvas """ ret = [] for e in data: x = translate_pos(geo_extrema[2], geo_extrema[3], e[F.GEO][P.LON], matrixdims[0]) y = translate_pos(geo_extrema[0], geo_extrema[1], e[F.GEO][P.LAT], matrixdims[1]) e.insert(F.POS, (x, y)) ret.append(e) logvv(f'Data updated: {e}') return ret def create_default_tile_pos_dict(data, geo_extrema, matrixdims): """ Returns dictionary with default position for every image by its ID. """ ret = dict() for d in data: x = translate_pos(geo_extrema[2], geo_extrema[3], d[F.GEO][P.LON], matrixdims[0]) y = translate_pos(geo_extrema[0], geo_extrema[1], d[F.GEO][P.LAT], matrixdims[1]) ret[d[F.ID]]=(x, y) logvv(f'create_default_tile_pos_dict: {ret}') return ret def create_playbook(posdict): """ Returns list of dict [delta1, delta2, ...] with changes (valid 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 default item positions 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: Check if there is already an item at the position of the actual image. If true, change its position by the displacement rule. - Place the image of the actual item (a) at its default position in the snapshot - Create an 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 posdict: prev_snapshot = snapshot pos = posdict[a] snapshot = make_pos_free(pos, snapshot, matrixdims) return ret def make_pos_free(pos, snapshot, matrixdims): """ Search for an existing tile in the snapshot at position pos. If exists, the new position will be calculated. With an recursive call, this new position will be checked, too. """ act = pos ret = snapshot for i in ret: if (ret[i] == act): shifted_pos = shifting(act, matrixdims) ret = make_pos_free(shifted_pos, snapshot, matrixdims) ret[i] = shifted_pos break return ret def shifting(pos, matrixdims): """Strategy to shift for a pos tuple. 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. Can result in a shift outside of the canvas. Returns the new position as (x, y) tuple. """ if pos[P.Y] > (matrixdims[P.Y] / 2): return (pos[P.X], pos[P.Y] + 1) else: return (pos[P.X], pos[P.Y] - 1) # def add_pos_column_to_data(): # """Enhance data with original pos.""" # global data # geo_extrema = find_geo_extrema(data) # data = append_tile_pos(data, geo_extrema, matrixdims) def main(): global args global defaultpos global data args=parse_args() data = read_importdata() geo_extrema = find_geo_extrema(data) log(f'matrixdims={matrixdims}') defaultpos = create_default_tile_pos_dict(data, geo_extrema, matrixdims) log("Finished.") if __name__ == "__main__": main()