Arbeitsverzeichnis für Pendel-Redaktion. Beinhaltet Skripte sowie alle Pendel-Inhalte.
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

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()