You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
317 lines
8.8 KiB
317 lines
8.8 KiB
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() |