Compare commits

..

No commits in common. '462ce7eb998783eae6c08a5c535f487afedb8c85' and '48a12e4f37481b7260ada2b8b379b15507137029' have entirely different histories.

  1. 8
      README.md
  2. BIN
      out/20250212-175748-1692-X-T50(1).jpg
  3. BIN
      out/20250409-182840-1896-X-T50.jpg
  4. BIN
      out/20250602-172821-1999-X-T50(1).jpg
  5. 4
      out/gps.csv
  6. BIN
      out/tile_20250212-175748-1692-X-T50(1).jpg
  7. BIN
      out/tile_20250409-182840-1896-X-T50.jpg
  8. BIN
      out/tile_20250602-172821-1999-X-T50(1).jpg
  9. 1
      site_builder/.gitignore
  10. BIN
      site_builder/__pycache__/main.cpython-313.pyc
  11. BIN
      site_builder/__pycache__/test_find_extrema.cpython-313-pytest-8.4.0.pyc
  12. BIN
      site_builder/__pycache__/test_find_geo_extrema.cpython-313-pytest-8.4.0.pyc
  13. BIN
      site_builder/__pycache__/test_main.cpython-313-pytest-8.4.0.pyc
  14. BIN
      site_builder/__pycache__/test_main.cpython-313.pyc
  15. BIN
      site_builder/__pycache__/test_main_unittests.cpython-313-pytest-8.4.0.pyc
  16. BIN
      site_builder/__pycache__/test_read_csv_to_dict.cpython-313-pytest-8.4.0.pyc
  17. BIN
      site_builder/__pycache__/test_translate_pos.cpython-313-pytest-8.4.0.pyc
  18. 7
      site_builder/constants/datafields.py
  19. 6
      site_builder/constants/pos.py
  20. 208
      site_builder/main.py
  21. 12
      site_builder/test_find_geo_extrema.py
  22. 130
      site_builder/test_make_pos_free.py
  23. 6
      site_builder/test_read_csv_to_dict.py
  24. 20
      site_builder/test_shifting.py

8
README.md

@ -221,14 +221,6 @@ Die so hochgeladenen Daten werden aber noch nicht verwendet.
> ❗HINWEIS > ❗HINWEIS
> Damit nicht gitea die SSH-Befehle interpretiert, müssen alle per SSH ausgeführten Commands in der Datei `.ssh/authorized_keys` eingetragen sein. > Damit nicht gitea die SSH-Befehle interpretiert, müssen alle per SSH ausgeführten Commands in der Datei `.ssh/authorized_keys` eingetragen sein.
### Troubleshooting
**F Neues Bild/Tile wird nach dem deploy nicht angezeigt**
**A** Kann mehrere Ursachen haben:
* Dateiname von Bild `<IMAGE>` und Tile `tile_<IMAGE>` stimmen nicht exakt übereinstimmen
* Dateiname in `gps.csv` stimmt nicht exakt überein
Passiert schnell, wenn mehrere Bildversionen hochgeladen werden. Dann per SSH aller Bilder mit Zeitstempel löschen und per import den Eintrag erneut erzeugen und hochladen.
### Hintergrundinfos ### Hintergrundinfos
#### Datei gps.csv #### Datei gps.csv

BIN
out/20250212-175748-1692-X-T50(1).jpg

Binary file not shown.

Before

Width:  |  Height:  |  Size: 177 KiB

After

Width:  |  Height:  |  Size: 177 KiB

BIN
out/20250409-182840-1896-X-T50.jpg

Binary file not shown.

Before

Width:  |  Height:  |  Size: 223 KiB

BIN
out/20250602-172821-1999-X-T50(1).jpg

Binary file not shown.

Before

Width:  |  Height:  |  Size: 204 KiB

4
out/gps.csv

@ -108,6 +108,4 @@
20240304-201704-110-X-S10_2.jpg 50.1721638888889 9.11536694444444 Pendel CIIX Neue Feldstärke 20240304-201704-110-X-S10_2.jpg 50.1721638888889 9.11536694444444 Pendel CIIX Neue Feldstärke
20241218-170830-1541-X-T50_v1.jpg 50.1192902666667 8.73992721666667 Pendel CIX Irgendwo am Osthafen 20241218-170830-1541-X-T50_v1.jpg 50.1192902666667 8.73992721666667 Pendel CIX Irgendwo am Osthafen
20250203-210719-1685-X-T50.jpg 50.1328722666667 8.91704896666667 Pendel CX Ein paar Menschenpaare 20250203-210719-1685-X-T50.jpg 50.1328722666667 8.91704896666667 Pendel CX Ein paar Menschenpaare
20250212-175748-1692-X-T50(1).jpg 50.1075388888889 8.71126138888889 Pendel CXI Zwischen Brücken 20250212-175748-1692-X-T50(1).jpg 50.1075388888889 8.71126138888889 Pendel CX Zwischen Brücken
20250409-182840-1896-X-T50.jpg 50.1117265333333 8.70644641666667 Pendel CXII Passantin mit Buch
20250602-172821-1999-X-T50(1).jpg 50.1096861111111 8.66920555555556 Pendel CXIII Trockenraum

1 20170313-180536-DSCF4779.jpg 50.1555543999528 8.95522409999722 Pendel I Baerlauchwald
108 20240304-201704-110-X-S10_2.jpg 50.1721638888889 9.11536694444444 Pendel CIIX Neue Feldstärke
109 20241218-170830-1541-X-T50_v1.jpg 50.1192902666667 8.73992721666667 Pendel CIX Irgendwo am Osthafen
110 20250203-210719-1685-X-T50.jpg 50.1328722666667 8.91704896666667 Pendel CX Ein paar Menschenpaare
111 20250212-175748-1692-X-T50(1).jpg 50.1075388888889 8.71126138888889 Pendel CXI Pendel CX Zwischen Brücken
20250409-182840-1896-X-T50.jpg 50.1117265333333 8.70644641666667 Pendel CXII Passantin mit Buch
20250602-172821-1999-X-T50(1).jpg 50.1096861111111 8.66920555555556 Pendel CXIII Trockenraum

BIN
out/tile_20250212-175748-1692-X-T50(1).jpg

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 24 KiB

BIN
out/tile_20250409-182840-1896-X-T50.jpg

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

BIN
out/tile_20250602-172821-1999-X-T50(1).jpg

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

1
site_builder/.gitignore vendored

@ -1 +0,0 @@
__pycache__/

BIN
site_builder/__pycache__/main.cpython-313.pyc

Binary file not shown.

BIN
site_builder/__pycache__/test_find_extrema.cpython-313-pytest-8.4.0.pyc

Binary file not shown.

BIN
site_builder/__pycache__/test_find_geo_extrema.cpython-313-pytest-8.4.0.pyc

Binary file not shown.

BIN
site_builder/__pycache__/test_main.cpython-313-pytest-8.4.0.pyc

Binary file not shown.

BIN
site_builder/__pycache__/test_main.cpython-313.pyc

Binary file not shown.

BIN
site_builder/__pycache__/test_main_unittests.cpython-313-pytest-8.4.0.pyc

Binary file not shown.

BIN
site_builder/__pycache__/test_read_csv_to_dict.cpython-313-pytest-8.4.0.pyc

Binary file not shown.

BIN
site_builder/__pycache__/test_translate_pos.cpython-313-pytest-8.4.0.pyc

Binary file not shown.

7
site_builder/constants/datafields.py

@ -1,7 +0,0 @@
# Field position for data items
ID = 0
FILE = 1
GEO = 2
TITLE = 3
DESCRIPTION = 4
POS = 5

6
site_builder/constants/pos.py

@ -1,6 +0,0 @@
# Universal geo pos tuple
LAT = 0
LON = 1
X = 0
Y = 1

208
site_builder/main.py

@ -2,45 +2,19 @@ import argparse
import csv import csv
import os.path as path import os.path as path
import re import re
import constants.datafields as F
import constants.pos as P
importdir = 'images' importdir = 'images'
datafile = path.join(importdir, 'gps.csv') datafile = path.join(importdir, 'gps.csv')
args = None args = None
# ---------- # Imported data with appended attributes as list of lists
# Image data # Items:
# ----------
# 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 # - Image file name
# - lon # - lon, lat
# - lat # - title, description
# - title # - x, y: Default tile position
# - description
# - pos as (x, y): Default tile position
data = None 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 # Has message in error case
error = None error = None
@ -73,15 +47,8 @@ def exit_with_error(message):
def read_csv_to_dict(csv_file): def read_csv_to_dict(csv_file):
""" """
Reads a tab-separated CSV file and returns a list of dictionaries, one per row Reads a CSV file and returns a list of dictionaries, one per row.
in the full data item size. Placeholder for unfilled fields is None. Assumes the first row contains headers.
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 In error case, returns None
""" """
global message global message
@ -90,7 +57,6 @@ def read_csv_to_dict(csv_file):
reader = csv.reader(f, skipinitialspace=False, delimiter='\t') reader = csv.reader(f, skipinitialspace=False, delimiter='\t')
log(f"Read lines from {csv_file}") log(f"Read lines from {csv_file}")
id = 0
for row in reader: for row in reader:
logvv(f"Processing row: {row}") logvv(f"Processing row: {row}")
@ -108,21 +74,9 @@ def read_csv_to_dict(csv_file):
if len(geo) != 2: if len(geo) != 2:
message = f'Invalid loc/lon: {row[1]}' message = f'Invalid loc/lon: {row[1]}'
id += 1 values = [ row[0], float(geo[0]), float(geo[1]), row[2], row[3] ]
# 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) data.append(values)
logvv(f"Read row : {values}")
logvv(f"Processed item : {values}")
if len(data) == 0: if len(data) == 0:
message = f'Empty file: {csv_file}' message = f'Empty file: {csv_file}'
@ -135,7 +89,7 @@ def read_csv_to_dict(csv_file):
def read_importdata(): def read_importdata():
data = None global data
if not path.exists(args.importdir): if not path.exists(args.importdir):
exit_with_error(f"Error: Import directory '{args.importdir}' does not exist.") exit_with_error(f"Error: Import directory '{args.importdir}' does not exist.")
@ -153,8 +107,6 @@ def read_importdata():
log("Read successful") log("Read successful")
return data
def find_geo_extrema(data): def find_geo_extrema(data):
""" """
@ -165,24 +117,20 @@ def find_geo_extrema(data):
if len(data) == 0: if len(data) == 0:
return None return None
lonmin = lonmax = data[0][F.GEO][P.LON] lonmin = lonmax = data[0][1]
latmin = latmax = data[0][F.GEO][P.LAT] latmin = latmax = data[0][2]
for entry in data[1:]: for entry in data[1:]:
if entry[F.GEO][P.LON] < lonmin: if entry[1] < lonmin:
lonmin = entry[F.GEO][P.LON] lonmin = entry[1]
if entry[F.GEO][P.LON] > lonmax: if entry[1] > lonmax:
lonmax = entry[F.GEO][P.LON] lonmax = entry[1]
if entry[F.GEO][P.LAT] < latmin: if entry[2] < latmin:
latmin = entry[F.GEO][P.LAT] latmin = entry[2]
if entry[F.GEO][P.LAT] > latmax: if entry[2] > latmax:
latmax = entry[F.GEO][P.LAT] latmax = entry[2]
ret = (latmin, latmax, lonmin, lonmax) return (lonmin, lonmax, latmin, latmax)
log(f'find_geo_extrema={ret}')
return ret
def translate_pos(min, max, pos, size): def translate_pos(min, max, pos, size):
@ -190,126 +138,36 @@ def translate_pos(min, max, pos, size):
return round((pos - min) / (max - min) * size) return round((pos - min) / (max - min) * size)
def append_tile_pos(data, geo_extrema, matrixdims):
def append_tile_pos_DEPTRECATED(data, geo_extrema, matrixdims):
""" """
Adds one column with (x, y) position (integers) of the tile in the canvas Adds two columns with x and y position (integer) of the tile in the canvas
""" """
ret = [] ret = []
for e in data: for e in data:
x = translate_pos(geo_extrema[2], geo_extrema[3], e[F.GEO][P.LON], matrixdims[0]) x = translate_pos(geo_extrema[2], geo_extrema[3], e[2], matrixdims[0])
y = translate_pos(geo_extrema[0], geo_extrema[1], e[F.GEO][P.LAT], matrixdims[1]) y = translate_pos(geo_extrema[0], geo_extrema[1], e[1], matrixdims[1])
e.insert(F.POS, (x, y)) e.append(x)
e.append(y)
ret.append(e) ret.append(e)
logvv(f'Data updated: {e}') logvv(f'Data updated: {e}')
return ret return ret
def create_default_tile_pos_dict(data, geo_extrema, matrixdims): def calculate_orig_pos():
""" global data
Returns dictionary with default position for every image by its ID. geo_extrema = find_geo_extrema(data)
""" data = append_tile_pos(data, geo_extrema, matrixdims)
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(): def main():
global args global args
global defaultpos
global data
args=parse_args() args=parse_args()
data = read_importdata() read_importdata()
geo_extrema = find_geo_extrema(data) calculate_orig_pos()
log(f'matrixdims={matrixdims}')
defaultpos = create_default_tile_pos_dict(data, geo_extrema, matrixdims)
log("Finished.") log("Finished.")

12
site_builder/test_find_geo_extrema.py

@ -10,11 +10,11 @@ def patch_log(monkeypatch):
def test_find_geo_extrama_simpple(): def test_find_geo_extrama_simpple():
data = [ data = [
[0, 'dummy', (50.1, 8.1) ], ['dummy', 50.1, 8.1 ],
[1, 'dummy', (50.2, 8.2) ], ['dummy', 50.2, 8.2 ],
[2, 'dummy', (50.3, 8.4) ], ['dummy', 50.3, 8.4 ],
[3, 'dummy', (49.3, 8.4) ], ['dummy', 49.3, 8.4 ],
[4, 'dummy', (49.3, 7.4) ] ['dummy', 49.3, 7.4 ]
] ]
result = main.find_geo_extrema(data) result = main.find_geo_extrema(data)
@ -24,7 +24,7 @@ def test_find_geo_extrama_simpple():
def test_find_geo_extrama_single(): def test_find_geo_extrama_single():
data = [ data = [
[0, 'dummy', (50.1, 8.1) ] ['dummy', 50.1, 8.1 ]
] ]
result = main.find_geo_extrema(data) result = main.find_geo_extrema(data)

130
site_builder/test_make_pos_free.py

@ -1,130 +0,0 @@
import pytest
import main
#########
# In order to reduce the dependency of method shifting(),
# the position is not explicitly checked.
#########
@pytest.fixture(autouse=True)
def patch_log(monkeypatch):
monkeypatch.setattr(main, "log", lambda msg: None)
monkeypatch.setattr(main, "logvv", lambda msg: None)
def test_single_item_no_match():
matrixdims = (6, 3)
pos = (0,0)
before = {1:pos}
after = main.make_pos_free((2, 3), before, matrixdims)
assert after[1] == pos
def test_single_item_match_at_0_0():
pos = (0,0)
before = {1:pos}
matrixdims = (6, 3)
after = main.make_pos_free(pos, before, matrixdims)
assert after[1] != pos
def test_single_item_match_at_horizont():
pos = (0, 1)
before = {1:pos}
# Y
# 0
# 1 <--- Horizon
# 2
matrixdims = (99, 2)
after = main.make_pos_free(pos, before, matrixdims)
assert after[1] != pos
def test_single_item_match_at_upper():
pos = (0, 0)
before = {1:pos}
# Y
# 0
# 1 <--- Horizon
# 2
matrixdims = (99, 2)
after = main.make_pos_free(pos, before, matrixdims)
assert after[1] != pos
def test_single_item_match_at_lower():
pos = (0, 2)
before = {1:pos}
# Y
# 0 ^
# 1 <--- Horizon | Lower | Upper
# 2 V
matrixdims = (99, 2)
after = main.make_pos_free(pos, before, matrixdims)
assert after[1] != pos
def test_first_item():
pos = (1,1)
matrixdims = (6, 3)
before = []
after = main.make_pos_free(pos, before, matrixdims)
assert len(after) == 0
def test_check_direct_neighbours_upwards():
matrixdims = (1000, 1000)
pos = (100,200)
before = {
1:(pos[main.P.X] , pos[main.P.Y] ),
2:(pos[main.P.X] , pos[main.P.Y]-1 ),
}
after = main.make_pos_free(pos, before, matrixdims)
for i in after:
for j in after:
if i == j:
continue
assert after[j] != after[i], f"i={i}, j={j}: Two items may not have the same position: {after[i]}"
def test_multiple_item_no_match():
matrixdims = (6, 3)
pos = (1,1)
before = {
1:(pos[main.P.X]+1, pos[main.P.Y] ),
2:(pos[main.P.X]-1, pos[main.P.Y] ),
3:(pos[main.P.X] , pos[main.P.Y]+1),
4:(pos[main.P.X] , pos[main.P.Y]-1),
5:(pos[main.P.X]+1, pos[main.P.Y]+1),
6:(pos[main.P.X]+1, pos[main.P.Y]-1),
7:(pos[main.P.X]-1, pos[main.P.Y]+1),
8:(pos[main.P.X]-1, pos[main.P.Y]-1),
}
after = main.make_pos_free(pos, before, matrixdims)
for i in after:
for j in after:
if i == j:
continue
assert after[j] != after[i], f"i={i}, j={j}: Two items may not have the same position: {after[i]}"

6
site_builder/test_read_csv_to_dict.py

@ -18,8 +18,8 @@ def test_read_csv_to_dict_basic(tmp_path):
result = main.read_csv_to_dict(str(file_path)) result = main.read_csv_to_dict(str(file_path))
expected = [ expected = [
[1, 'img1', (12.34, 56.78), 'foo', 'bar', None], ['img1', 12.34, 56.78, 'foo', 'bar'],
[2, 'img2', (90.12, 34.56), 'baz', 'qux', None], ['img2', 90.12, 34.56, 'baz', 'qux'],
] ]
assert result == expected assert result == expected
@ -39,7 +39,7 @@ def test_read_csv_to_dict_extra_spaces(tmp_path):
file_path.write_text(content, encoding="utf-8") file_path.write_text(content, encoding="utf-8")
result = main.read_csv_to_dict(str(file_path)) result = main.read_csv_to_dict(str(file_path))
expected = [[1, 'img3', (3.23, 4.56), 'foo', 'bar', None]] expected = [['img3', 3.23, 4.56, 'foo', 'bar']]
assert result == expected assert result == expected

20
site_builder/test_shifting.py

@ -1,20 +0,0 @@
import pytest
import main
@pytest.fixture(autouse=True)
def patch_log(monkeypatch):
monkeypatch.setattr(main, "log", lambda msg: None)
monkeypatch.setattr(main, "logvv", lambda msg: None)
def test_shifting_top():
assert main.shifting((2,3), (4,5)) == (2,4)
def test_shifting_mid():
assert main.shifting((20,24), (21,49)) == (20,23)
def test_shifting_botom():
assert main.shifting((20,26), (21,49)) == (20,27)
Loading…
Cancel
Save