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.
958 lines
29 KiB
958 lines
29 KiB
import argparse |
|
from geopy.geocoders import Nominatim |
|
import math |
|
import os |
|
import re |
|
import shutil |
|
# from typing_extensions import TypeVarTuple |
|
from PIL import Image, ImageOps |
|
from datetime import datetime |
|
|
|
from PIL.ExifTags import TAGS |
|
|
|
WORK_DIR = 'work' |
|
HUGO_ROOT = '..' |
|
HUGO_STATIC_DIR = '../static' |
|
IMAGE_ROOT_DIR = 'images/gallery' |
|
DROPIN_DIR = '../../dropin' |
|
FULLS_DIR = 'fulls' |
|
SVGS_DIR = 'svgs' |
|
THUMBS_DIR = 'thumbs' |
|
IMAGES_DIR = '../../images' |
|
SPOT_DIR = 'data/spot' |
|
|
|
HOME_LAT = 50.18573 |
|
HOME_LON = 9.14265 |
|
|
|
COLOR_CIRCLE = "#cc9966" |
|
COLOR_DOT = "#996600" |
|
|
|
# Radius around Home in meters |
|
RADIUS = 15000 |
|
|
|
# pixel = meter * scale |
|
SCALE = 0.1 |
|
|
|
# Number oy images per spot |
|
SPOTS_SIZE = 9 |
|
|
|
images = [] |
|
bundles = [] |
|
|
|
# program arguments |
|
args = None |
|
|
|
|
|
|
|
|
|
def create_banner_svg(): |
|
|
|
""" |
|
Create banner SVG files based on the build bundles. |
|
All things will be done in the locale work folder. |
|
SVG has following coordinate system: |
|
|
|
(0,0) |
|
+---------------------> +x |
|
| |
|
| +--------------+ (2*homx)+margx |
|
| | | |
|
| | (homx,homy) | |
|
| | + | |
|
| | | |
|
| | | |
|
| +--------------+ |
|
| (2*homy)+margy |
|
| |
|
V |
|
+y |
|
""" |
|
# print(f"Creating banner SVG file") |
|
|
|
# Circle should not hit image edges |
|
margin = int(round(RADIUS / 20.0)) |
|
|
|
# All calculations in a home centered coordinte system (not in a SVG coordinate system), |
|
# unscaled and without margin |
|
|
|
# Initialize |
|
xmax = RADIUS |
|
ymax = RADIUS |
|
xmin = -xmax |
|
ymin = -ymax |
|
|
|
# find canvas size (without margin): all image points have to be on the canvas |
|
# TODO Not useful as long the points will not be shown |
|
for image in images: |
|
|
|
if image['x'] > xmax: |
|
xmax = image['x'] |
|
|
|
if image['x'] < xmin: |
|
xmin = image['x'] |
|
|
|
if image['y'] > ymax: |
|
ymax = image['y'] |
|
|
|
if image['y'] < ymin: |
|
ymin = image['y'] |
|
|
|
radius15 = RADIUS |
|
radiusCenter = int(round(RADIUS * 0.02)) |
|
offsetx = -xmin + margin |
|
offsety = -ymin + margin |
|
|
|
opacbundlemax = 0.4 |
|
opacbundlemin = 0.0001 |
|
opacbundlestep = round((opacbundlemax - opacbundlemin) / len(bundles),4) |
|
|
|
# opacimagemax = 0.8 |
|
# opacimagemin = 0.2 |
|
|
|
radius_dot =int(round(RADIUS * 0.01)) |
|
|
|
print(f"Creating banner SVG, corners=({xmin},{ymin}),({xmax},{ymax})") |
|
|
|
name = os.path.join(WORK_DIR, IMAGE_ROOT_DIR, SVGS_DIR, "banner.svg") |
|
|
|
with open(name, 'w') as file: |
|
file.write(f"<?xml version=\"1.0\" encoding=\"utf-8\"?>\n") |
|
file.write(f"<!DOCTYPE svg PUBLIC \"-//W3C//DTD SVG 1.1//EN\" \"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd\">\n") |
|
file.write(f"<svg version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" \n") |
|
file.write(f"viewBox=\"0 0 {scale(offsetx + xmax + margin)} {scale(offsety + ymax + margin)}\" xml:space=\"preserve\">\n") |
|
file.write(f"<circle cx=\"{scale(offsetx)}\" cy=\"{scale(offsety)}\" r=\"{scale(radius15)}\" fill=\"#eeeeee\"> \n ") |
|
file.write(f"</circle>\n") |
|
file.write(f"<circle cx=\"{scale(offsetx)}\" cy=\"{scale(offsety)}\" r=\"{scale(radiusCenter)}\" fill=\"#ffffff\"> \n ") |
|
file.write(f"</circle>\n") |
|
|
|
for i, bundle in enumerate(bundles): |
|
|
|
log(f" bundle {i} with {len(bundle['images']) + 1} images") |
|
|
|
file.write(f"<circle cx=\"{scale(offsetx + bundle['x'])}\" cy=\"{scale(offsety + bundle['y'])}\" " + |
|
f"r=\"{scale(bundle['radius'] + radius_dot)}\" opacity=\"{opacbundlemax - (opacbundlestep * i)}\" fill=\"{COLOR_CIRCLE}\">\n") |
|
file.write(f"</circle>") |
|
|
|
file.write(f"</svg>") |
|
|
|
|
|
def create_svgs(): |
|
""" |
|
Create SVGs files based on the build bundles. |
|
All things will be done in the locale work folder. |
|
SVG has following coordinate system: |
|
|
|
(0,0) |
|
+---------------------> +x |
|
| |
|
| +--------------+ (2*homx)+margx |
|
| | | |
|
| | (homx,homy) | |
|
| | + | |
|
| | | |
|
| | | |
|
| +--------------+ |
|
| (2*homy)+margy |
|
| |
|
V |
|
+y |
|
""" |
|
|
|
print(f"create_svgs()") |
|
|
|
|
|
# Circle should not hit image edges |
|
margin = int(round(RADIUS / 10.0)) |
|
|
|
# All calculations in a home centered coordinte system (not in a SVG coordinate system), |
|
# unscaled and without margin |
|
|
|
# Initialize |
|
|
|
|
|
opacbundlemax = 0.4 |
|
opacbundlemin = 0.0001 |
|
opacbundlestep = round((opacbundlemax - opacbundlemin) / len(bundles),4) |
|
|
|
opacimagemax = 0.8 |
|
opacimagemin = 0.2 |
|
|
|
opacmidmax = 0.9 |
|
opacmidmin = 0.4 |
|
|
|
radius_dot =int(round(RADIUS * 0.01)) |
|
|
|
for i, bundle in enumerate(bundles): |
|
|
|
# log(f"create_svgs() bundle {bundle['x']} {bundle['y']} {bundle['radius']}") |
|
|
|
# Always show center |
|
xmax = max(bundle['x'] + bundle['radius'], 0) |
|
ymax = max(bundle['y'] + bundle['radius'], 0) |
|
xmin = min(bundle['x'] - bundle['radius'], 0) |
|
ymin = min(bundle['y'] - bundle['radius'], 0) |
|
|
|
log(f" bundle: {bundle['image']['date']}: ({xmin},{ymin},{xmax},{ymax})") |
|
# log(f" ({xmax-xmin},{ymax-ymin}) - {round((xmax-xmin) / (ymax-ymin),1)}, ") |
|
|
|
# All images incl. main bundle image |
|
images = bundle['images'].copy() |
|
images.append(bundle['image']) |
|
|
|
# find canvas size (without margin): all image points have to be on the canvas |
|
for image in images: |
|
|
|
if image['x'] > xmax: |
|
xmax = image['x'] |
|
|
|
if image['x'] < xmin: |
|
xmin = image['x'] |
|
|
|
if image['y'] > ymax: |
|
ymax = image['y'] |
|
|
|
if image['y'] < ymin: |
|
ymin = image['y'] |
|
|
|
# log(f" now ({xmin},{ymin},{xmax},{ymax}) / ({xmax-xmin},{ymax-ymin}) - {round((xmax-xmin) / (ymax-ymin),1)}, ") |
|
|
|
# Original aspect ratio: 16:10 |
|
ratio = 1.8 |
|
ytarget = ( xmax - xmin ) / ratio |
|
|
|
# Expand y |
|
if ( ymax - ymin ) < ytarget: |
|
# log(f" Expanding y with ytarget={ytarget} ") |
|
ymaxold = ymax |
|
ymax = int(round(ymax + ((ytarget - ( ymax - ymin )) / 2))) |
|
ymin = int(round(ymin - ((ytarget - ( ymaxold - ymin )) / 2))) |
|
|
|
# Expand x |
|
else: |
|
xtarget = int(round(( ymax - ymin ) * ratio)) |
|
# log(f" Expanding x with xtarget={xtarget} ") |
|
xmaxold = xmax |
|
xmax = int(round(xmax + ((xtarget - ( xmax - xmin )) / 2))) |
|
xmin = int(round(xmin - ((xtarget - ( xmaxold - xmin )) / 2))) |
|
|
|
# log(f" -->({xmin},{ymin}),({xmax},{ymax}) ") |
|
# log(f" ({xmax-xmin},{ymax-ymin}) - {round((xmax-xmin) / (ymax-ymin),1)}, ") |
|
|
|
radius15 = RADIUS |
|
home_radius_center = int(round(RADIUS * 0.02)) |
|
offsetx = -xmin + margin |
|
offsety = -ymin + margin |
|
|
|
name = os.path.join(WORK_DIR, IMAGE_ROOT_DIR, SVGS_DIR, f"{bundle['image']['date']}.svg") |
|
|
|
log(f" --> Spot SVG, corners=({xmin},{ymin}),({xmax},{ymax}") |
|
|
|
with open(name, 'w') as file: |
|
|
|
|
|
file.write(f"<?xml version=\"1.0\" encoding=\"utf-8\"?>\n") |
|
file.write(f"<!DOCTYPE svg PUBLIC \"-//W3C//DTD SVG 1.1//EN\" \"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd\">\n") |
|
file.write(f"<svg version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" \n") |
|
# file.write(f" width=\"1440\" heigth=\"900\" \n") |
|
file.write(f"viewBox=\"0 0 {scale(offsetx + xmax + margin)} {scale(offsety + ymax + margin)}\" xml:space=\"preserve\">\n") |
|
|
|
file.write(f"<circle cx=\"{scale(offsetx)}\" cy=\"{scale(offsety)}\" r=\"{scale(radius15)}\" fill=\"#eeeeee\"> \n ") |
|
file.write(f"</circle>\n") |
|
|
|
file.write(f"<circle cx=\"{scale(offsetx)}\" cy=\"{scale(offsety)}\" r=\"{scale(home_radius_center)}\" fill=\"#ffffff\"> \n ") |
|
file.write(f"</circle>\n") |
|
|
|
file.write(f"<circle cx=\"{scale(offsetx + bundle['x'])}\" cy=\"{scale(offsety + bundle['y'])}\" " + |
|
f"r=\"{scale(bundle['radius'] + radius_dot)}\" opacity=\"{opacbundlemax - (opacbundlestep * i)}\" fill=\"{COLOR_CIRCLE}\">\n") |
|
|
|
r_from = scale((bundle['radius'] + radius_dot) * 0.995) |
|
r_to = scale((bundle['radius'] + radius_dot) * 1.005) |
|
file.write(f"<animate id=\"step1\" \n") |
|
file.write(f"begin=\"0s;step2.end\" dur=\"1s\" \n") |
|
file.write(f"attributeName=\"r\" from=\"{r_from}\" to=\"{r_to}\" \n") |
|
file.write(f"/>\n") |
|
|
|
file.write(f"<animate id=\"step2\" \n") |
|
file.write(f"begin=\"step1.end\" dur=\"1s\"\n") |
|
file.write(f"attributeName=\"r\" from=\"{r_to}\" to=\"{r_from}\" \n") |
|
file.write(f"/>\n") |
|
|
|
|
|
file.write(f"</circle>") |
|
|
|
# Center dot of bundle circle |
|
file.write(f"<circle cx=\"{scale(offsetx + bundle['x'])}\" cy=\"{scale(offsety + bundle['y'])}\" " + |
|
f"r=\"{scale(bundle['radius'] * 0.02 )}\" opacity=\"{opacmidmax - (opacbundlestep * i)}\" fill=\"#ffffff\">\n") |
|
file.write(f"</circle>") |
|
|
|
opacimagestep = round((opacimagemax - opacimagemin) / len(images),2) |
|
|
|
for i, image in enumerate(images): |
|
file.write(f"<circle cx=\"{scale(offsetx + image['x'])}\" cy=\"{scale(offsety + image['y'])}\" " + |
|
f"r=\"{scale(radius_dot)}\" opacity=\"{opacimagemax - (opacimagestep * i)}\" fill=\"{COLOR_DOT}\">\n") |
|
file.write(f"</circle>") |
|
|
|
file.write(f"</svg>") |
|
|
|
print("..done") |
|
|
|
|
|
def scale(value): |
|
return int(value * SCALE) |
|
|
|
|
|
def get_exif(fn, warns): |
|
""" |
|
Return dictionary with EXIF data and the optinal title. |
|
""" |
|
ret = {} |
|
|
|
with Image.open(fn) as i: |
|
info = i._getexif() |
|
for tag, value in info.items(): |
|
decoded = TAGS.get(tag, tag) |
|
ret[decoded] = value |
|
|
|
xmp = i.getxmp() |
|
title = False |
|
try: |
|
title = xmp['xmpmeta']['RDF']['Description'][0]['title']['Alt']['li']['text'] |
|
# log(f"Found title for {fn}: {ret['title']}") |
|
except: |
|
pass |
|
|
|
if not title: |
|
try: |
|
title = xmp['xmpmeta']['RDF']['Description'][0]['title'] |
|
# log(f"Found title for {fn}: {ret['title']}") |
|
except: |
|
pass |
|
|
|
if title: |
|
# log(f"{fn} Title={title}") |
|
ret['title'] = title |
|
else: |
|
if args.verbose: |
|
warns.append(f"No title found: {fn}") |
|
|
|
return ret |
|
|
|
|
|
def get_geo(gpsInfo): |
|
lat = 0 |
|
lon = 0 |
|
|
|
lat = (((gpsInfo[2][2] / 60.0) + gpsInfo[2][1]) / 60.0) + gpsInfo[2][0] |
|
if gpsInfo[1] == 'S': |
|
lat = -lat |
|
|
|
lon = (((gpsInfo[4][2] / 60.0) + gpsInfo[4][1]) / 60.0) + gpsInfo[4][0] |
|
if gpsInfo[3] == 'W': |
|
lon = -lon |
|
|
|
# log(f"get_geo: gpsInfo={gpsInfo[1]}, {gpsInfo[2]}, {gpsInfo[3]}, {gpsInfo[4]} --> lat={lat}, lon={lon}") |
|
return lat, lon |
|
|
|
|
|
def get_pos(lat, lon): |
|
""" |
|
Calculates the distance in meters between the given geo point and the HOME. |
|
Calculation is based on https://www.kompf.de/gps/distcalc.html |
|
Returns a tuple (x, y) with distance in integer meters, based on ths SVG cordinate system |
|
| -y |
|
| |
|
|(0,0) |
|
-x -------+------- +x |
|
| |
|
| |
|
| +y |
|
""" |
|
x = int(round(71.5 * 1000.0 * (lon - HOME_LON))) |
|
y = int(round(111.3 * 1000.0 * (HOME_LAT - lat))) |
|
|
|
# log(f"get_pos: {lon} - {HOME_LON} --> x={x} ") |
|
# log(f"get_pos: {lat} - {HOME_LAT} --> y={y} ") |
|
|
|
return x, y |
|
|
|
|
|
def resolve_pos(x, y): |
|
""" |
|
Reverse calculation for get_pos() |
|
""" |
|
lon = round((x / (71.5 * 1000)) + HOME_LON, 6) |
|
lat = round(HOME_LAT - (y / (111.3 * 1000)), 6) |
|
|
|
log(f"resolve_pos: ({x},{y}) --> {lat}, {lon} ") |
|
|
|
return lat, lon |
|
|
|
|
|
def create_image_item(dir, file, warns): |
|
""" |
|
Create image dictionary for the given image file in the given directory. Both must exists. |
|
Returns dectionary with image item or, in error case, false. |
|
""" |
|
filename = os.path.join(dir, file) |
|
target_filename = os.path.join(IMAGES_DIR, file) |
|
tags = get_exif(filename, warns) |
|
|
|
# Invalid items gpsInfo=, (nan, nan, nan), , (nan, nan, nan) |
|
if 'GPSInfo' in tags and (tags['GPSInfo'][1] == 'N' or tags['GPSInfo'][1] == 'S'): |
|
lat, lon = get_geo(tags['GPSInfo']) |
|
else: |
|
lat, lon = (HOME_LAT, HOME_LON) |
|
warns.append(f"No GPS data: {file}") |
|
return False |
|
|
|
x, y = get_pos(lat, lon) |
|
|
|
# log(f"{name}") |
|
format = "%Y:%m:%d %H:%M:%S" |
|
dt = datetime.strptime(tags['DateTime'], format) |
|
|
|
image = dict(dt=dt, date=dt.strftime('%Y%m%d'),name=file, filename=target_filename, tags=tags, lat=lat, lon=lon, x=x, y=y) |
|
return image |
|
|
|
|
|
def format_image(name): |
|
""" |
|
Create full and thumb for one image from the dropin folder |
|
All things will be done in the locale work folder. |
|
Returns True in success case, otherwise False |
|
""" |
|
|
|
thumb_size = (360, 360) |
|
full_width = 1900 |
|
|
|
filename = os.path.join(DROPIN_DIR, name) |
|
|
|
try: |
|
with Image.open(filename) as im: |
|
|
|
# Must be rotated by exif Orientation, otherwise thumb and full will not be |
|
# shown the image in the correct rotation |
|
im = ImageOps.exif_transpose(im) |
|
|
|
# Size of the image in pixels (size of original image) |
|
# (This is not mandatory) |
|
width, height = im.size |
|
|
|
# Setting the points for cropped image |
|
if width > height: |
|
offset = int(round((width - height) / 2)) |
|
left = offset |
|
top = 0 |
|
right = height + offset |
|
bottom = height |
|
else: |
|
offset = int(round((height - width) / 2)) |
|
left = 0 |
|
top = offset |
|
right = width |
|
bottom = width + offset |
|
|
|
# Cropped image of above dimension |
|
# (It will not change original image) |
|
thumb = im.crop((left, top, right, bottom)) |
|
|
|
thumb.thumbnail(thumb_size) |
|
thumb.save(os.path.join(WORK_DIR, IMAGE_ROOT_DIR, THUMBS_DIR, name), "JPEG") |
|
|
|
concat = full_width/float(im.size[0]) |
|
size = int((float(im.size[1])*float(concat))) |
|
# print(f"concat,size={concat}, {size}") |
|
out = im.resize((full_width,size), Image.ANTIALIAS) |
|
out.save(os.path.join(WORK_DIR, IMAGE_ROOT_DIR, FULLS_DIR, name), "JPEG") |
|
|
|
except OSError: |
|
warn(f"\ncannot convert image {filename}! ") |
|
return False |
|
|
|
return True |
|
|
|
|
|
def process_images(): |
|
""" |
|
Process all images and create fulls and thumbnails from the dropin folder. |
|
Move the processed images into /images and rebuild hugo content |
|
All things will be done in the locale folder (work, dropin, images) |
|
""" |
|
log(f"Processing images from '{DROPIN_DIR}'") |
|
|
|
global images |
|
images = [] |
|
|
|
p = re.compile('\d{8}.*\.(jpg|jpeg)', re.IGNORECASE) |
|
|
|
new_files = [f for f in os.listdir(DROPIN_DIR) |
|
if os.path.isfile(os.path.join(DROPIN_DIR, f)) and |
|
p.match(f)] |
|
|
|
old_files = [f for f in os.listdir(IMAGES_DIR) |
|
if os.path.isfile(os.path.join(IMAGES_DIR, f)) and |
|
p.match(f)] |
|
log(f"Found {len(new_files)} images in {DROPIN_DIR} and {len(old_files)} images in {IMAGES_DIR}") |
|
|
|
if not new_files and not old_files: |
|
exit_with_good_bye(f"No images found.") |
|
|
|
cnt = 0 |
|
err = 0 |
|
warns =[] |
|
|
|
for name in new_files: |
|
cnt += 1 |
|
|
|
print(f".", end="", flush=True) |
|
|
|
# Can print a warning |
|
image = create_image_item(DROPIN_DIR, name, warns) |
|
|
|
if not image is False: |
|
# Can print a warning |
|
formatted = format_image(name) |
|
|
|
if image is False or formatted is False: |
|
char = "x" |
|
err += 1 |
|
|
|
else: |
|
char = '.' |
|
shutil.move( os.path.join(DROPIN_DIR, name), image['filename']) |
|
images.append(image) |
|
|
|
if cnt % 10 == 0: |
|
print(f"\b{char} ({cnt})", end="\n", flush=True) |
|
else: |
|
print(f"\b{char}", end="", flush=True) |
|
|
|
for name in old_files: |
|
cnt += 1 |
|
|
|
image = create_image_item(IMAGES_DIR, name, warns) |
|
|
|
# This should never happens |
|
if image is False: |
|
char = "X" |
|
err += 1 |
|
|
|
else: |
|
char = 'o' |
|
images.append(image) |
|
|
|
if cnt % 10 == 0: |
|
print(f"\b{char} ({cnt})", end="\n", flush=True) |
|
else: |
|
print(f"\b{char}", end="", flush=True) |
|
|
|
print("\n") |
|
|
|
if warns: |
|
for message in warns: |
|
warn(message) |
|
|
|
if not images: |
|
exit_with_good_bye("No valid images.") |
|
|
|
print(f"Total processed {len(new_files)} new and {len(old_files)} old images: {len(images)} OK, {err} ignored.") |
|
|
|
|
|
def create_bundles(): |
|
""" |
|
Bundle images to spots by global images list. Output |
|
is a list of spots with tpe dict: |
|
{ |
|
image=main_image, |
|
images=list_of_satellite_images, |
|
|
|
} |
|
All things will be done in the locale work folder. |
|
""" |
|
print(f"Create bundle images") |
|
|
|
global bundles |
|
bundles = [] |
|
|
|
if len(images)==0: |
|
exit_on_error("No images found to bundle") |
|
|
|
end = False |
|
|
|
left_images = images |
|
|
|
while not end: |
|
bundle = create_bundle(left_images) |
|
if bundle is None: |
|
end = True |
|
break |
|
|
|
bundles.append(bundle) |
|
|
|
log(f"removing main image={bundle['image']['name']}") |
|
|
|
left_images.remove(bundle['image']) |
|
for image in bundle['images']: |
|
log(f"removing bundle image={image['name']}") |
|
left_images.remove(image) |
|
|
|
# log(f"len={len(left_images)}") |
|
|
|
print(f"Processed {len(bundles)} bundles.") |
|
|
|
|
|
def calculate_circle(image, _images): |
|
""" |
|
Find the smallest circle for the images by finding the two point that are furhtermost from each other. |
|
Returns dict with x, y as distance from HOME and radius, all in meters as integer. |
|
""" |
|
|
|
# circle dimension. Initialization for case of single image bundle |
|
# 100.0 is no magic number, it's just a value to show something around the single image point |
|
max_radius = 100.0 |
|
x = image['x'] |
|
y = image['y'] |
|
|
|
images = _images.copy() |
|
images.append(image) |
|
|
|
log(f"calculate_circle for {len(images)} images", end="") |
|
|
|
|
|
for i, item1 in enumerate(images): |
|
# log(f"image {i}: {item1['x'],item1['y']}") |
|
for item2 in images[(i+1):]: |
|
|
|
x_dist = item2['x'] - item1['x'] |
|
y_dist = item2['y'] - item1['y'] |
|
radius = int(round(math.sqrt(x_dist**2 + y_dist**2) / 2.0)) |
|
if max_radius >= radius: |
|
continue |
|
|
|
max_radius = radius |
|
x = int(round(item1['x'] + (x_dist/2.0))) |
|
y = int(round(item1['y'] + (y_dist/2.0))) |
|
|
|
# log(f"image {i}: {item1['x'],item1['y']}") |
|
|
|
ret = dict(x=x,y=y,radius=max_radius) |
|
print(f"...done: {ret}") |
|
|
|
return ret |
|
|
|
|
|
def create_bundle(_images): |
|
|
|
if len(_images) == 0: |
|
return None |
|
|
|
# Find newest |
|
images.sort(key = lambda item: item.get("dt")) |
|
|
|
# Work with copy to prevent from |
|
newest = images[-1] |
|
|
|
log(f"Create bundle for newest={newest['name']}") |
|
|
|
# Add distance |
|
for image in images: |
|
image['dist'] = int(round(math.sqrt(((newest['x'] - image['x']) ** 2) + ((newest['y'] - image['y']) ** 2)))) |
|
|
|
# find nearbys. Be careful: newest must not be at first place, if there is a second image with dist == 0 |
|
images.sort(key = lambda item: item.get("dist")) |
|
|
|
# for image in images: |
|
# print(f"images: image={image['name']} dist={image['dist']}") |
|
|
|
bundle_images = [] |
|
|
|
# Exclde newest from list (must not be at first place, see above) |
|
for image in [i for i in images if i['name'] != newest['name']][:SPOTS_SIZE-1]: |
|
log(f"image={image['name']}") |
|
bundle_images.append(image) |
|
|
|
circle = calculate_circle(newest, bundle_images) |
|
|
|
bundle_images.sort(key = lambda item: item.get("date"), reverse=True) |
|
ret = dict(image=newest, images=bundle_images, x=circle['x'], y=circle['y'], radius=circle['radius']) |
|
|
|
log(f"Bundle created: x={ret['x']}, y={ret['y']}, radius={ret['radius']}") |
|
|
|
# print(f"newest ={ret['image']['date']}") |
|
# for i in ret['images']: |
|
# print(f"image ={i['dist']} , {i['date']}") |
|
|
|
return ret |
|
|
|
def get_address(latitude, longitude, language="de"): |
|
coordinates = f"{latitude}, {longitude}" |
|
|
|
# Switch off for develop pursoses |
|
if args.location_mockup: |
|
return "Entenhausen" |
|
|
|
try: |
|
geolocator = Nominatim(user_agent="de.kollegen.dierundestunde") |
|
location = geolocator.reverse(coordinates) |
|
log(f"get_address() {location}") |
|
parts = location.address.split(", ") |
|
if len(parts) == 8: |
|
return f"{parts[1]}, {parts[2]}" |
|
elif len(parts) >= 2: |
|
return f"{parts[0]}, {parts[1]}" |
|
else: |
|
return location.address |
|
except: |
|
print("geolocator failed") |
|
|
|
return "" |
|
|
|
|
|
def create_spots(): |
|
""" |
|
Create all spots content based on the build bundles. |
|
All things will be done in the locale work folder. |
|
""" |
|
|
|
print(f"Creating spots", end="") |
|
|
|
i = 0 |
|
|
|
for bundle in bundles: |
|
i += 1 |
|
|
|
filename = os.path.join(WORK_DIR, SPOT_DIR, f"{bundle['image']['date']}.yml") |
|
|
|
if (i % 2) == 0: |
|
orient = "orient-left" |
|
else: |
|
orient = "orient-right" |
|
|
|
# Get time range |
|
oldest = bundle['image']['date'] |
|
for image in bundle['images']: |
|
if image['date'] < oldest: |
|
oldest = image['date'] |
|
|
|
start = datetime.strptime(oldest, "%Y%m%d") |
|
end = datetime.strptime(bundle['image']['date'], "%Y%m%d") |
|
diff = end.date() - start.date() |
|
|
|
# Center |
|
lon, lat = resolve_pos(bundle['x'], bundle['y']) |
|
address = get_address(lon, lat) |
|
print(f"address={address}") |
|
|
|
# print(f"creating {filename}") |
|
with open(filename, 'w') as file: |
|
file.write(f"title: {bundle['image']['date'] }\n") |
|
file.write(f"headerstyle: \"style1 {orient} content-align-left image-position-center onscroll-image-fade-in\"\n") |
|
file.write(f"style: \"style2 medium lightbox onscroll-fade-in\"\n") |
|
file.write(f"header: \"{IMAGE_ROOT_DIR}/{SVGS_DIR}/{bundle['image']['date']}.svg\"\n") |
|
file.write(f"content: |\n") |
|
file.write(f" ➙ {address}<br>\n") |
|
file.write(f" R = {bundle['radius']} m<br>\n") |
|
file.write(f" Δ = {diff.days} Tage\n") |
|
file.write(f"circle:\n") |
|
file.write(f" - pos: \"({bundle['x']}, {bundle['y']})\"\n") |
|
file.write(f" radius: \"{bundle['radius']}\"\n") |
|
|
|
file.write(f"pictures:\n") |
|
|
|
write_picture_yaml(file, bundle['image']) |
|
|
|
for image in bundle['images']: |
|
write_picture_yaml(file, image) |
|
|
|
print(f".", end="") |
|
|
|
|
|
print(f" OK {len(bundles)} files created.") |
|
|
|
|
|
def write_picture_yaml(file, image): |
|
|
|
if 'title' in image['tags']: |
|
log(f"write_picture_yaml {image['date']} title={image['tags']['title']}") |
|
file.write(f" - title: \"{image['tags']['title']}\"\n") |
|
file.write(f" subtitle: \"{image['date']}\"\n") |
|
else: |
|
file.write(f" - title: \"{image['date']}\"\n") |
|
|
|
file.write(f" content: \"distance={image['dist']} m\"\n") |
|
file.write(f" distance: \"{image['dist']}\"\n") |
|
file.write(f" geo: \"{image['lon']}, {image['lat']}\"\n") |
|
file.write(f" pos: \"({str(image['x'])}, {str(image['y'])})\"\n") |
|
file.write(f" image: \"{os.path.join(IMAGE_ROOT_DIR, FULLS_DIR, image['name'])}\"\n") |
|
file.write(f" thumb: \"{os.path.join(IMAGE_ROOT_DIR, THUMBS_DIR, image['name'])}\"\n") |
|
file.write(f" button: \"Ansehen\"\n") |
|
|
|
|
|
def activate(): |
|
""" |
|
Active spots incl images from work folder to the hugo project. |
|
Old spots will be removed. |
|
This should only be processed in success case. |
|
""" |
|
print(f"Update hugo content", end="") |
|
|
|
spot_dir = os.path.join(HUGO_ROOT, SPOT_DIR) |
|
image_root_dir = os.path.join(HUGO_STATIC_DIR, IMAGE_ROOT_DIR) |
|
thumbs_dir = os.path.join(image_root_dir, THUMBS_DIR) |
|
fulls_dir = os.path.join(image_root_dir, FULLS_DIR) |
|
svgs_dir = os.path.join(image_root_dir, SVGS_DIR) |
|
|
|
if os.path.isdir(spot_dir): |
|
shutil.rmtree(spot_dir) |
|
|
|
shutil.copytree(os.path.join(WORK_DIR, SPOT_DIR), spot_dir) |
|
|
|
if not os.path.isdir(image_root_dir): |
|
os.makedirs(image_root_dir) |
|
|
|
if os.path.isdir(svgs_dir): |
|
shutil.rmtree(svgs_dir) |
|
|
|
shutil.copytree(os.path.join(WORK_DIR, IMAGE_ROOT_DIR, SVGS_DIR), svgs_dir) |
|
|
|
if os.path.isdir(thumbs_dir): |
|
shutil.rmtree(thumbs_dir) |
|
shutil.copytree(os.path.join(WORK_DIR, IMAGE_ROOT_DIR, THUMBS_DIR), thumbs_dir) |
|
|
|
if os.path.isdir(fulls_dir): |
|
shutil.rmtree(fulls_dir) |
|
shutil.copytree(os.path.join(WORK_DIR, IMAGE_ROOT_DIR, FULLS_DIR), fulls_dir) |
|
|
|
print("...done") |
|
|
|
|
|
def log(message, end="\n", flush=False): |
|
|
|
if args.verbose: |
|
print(message, end=end, flush=flush) |
|
|
|
|
|
def init_work(): |
|
""" |
|
Setup working directory. This should be the very first step. |
|
Fulls and thumbs directories will only be cleared with reset argument . |
|
""" |
|
log(f"Initialize directories") |
|
|
|
# The very firs time |
|
if not os.path.exists(DROPIN_DIR): |
|
print(f"Creating missing directory '{DROPIN_DIR}'") |
|
os.mkdir(DROPIN_DIR) |
|
|
|
if not os.path.exists(IMAGES_DIR): |
|
print(f"Creating missing directory '{IMAGES_DIR}'") |
|
os.mkdir(IMAGES_DIR) |
|
|
|
if not os.path.exists(WORK_DIR): |
|
print(f"Creating missing directory structure '{WORK_DIR}'") |
|
os.mkdir(WORK_DIR) |
|
|
|
fulls = os.path.join(WORK_DIR, IMAGE_ROOT_DIR, FULLS_DIR) |
|
if args.rebuild_all and os.path.exists(fulls): |
|
print(f"Removing {fulls}") |
|
shutil.rmtree(fulls) |
|
|
|
# The very firs time or in case of a rebuild all |
|
if not os.path.exists(fulls): |
|
os.makedirs(fulls) |
|
|
|
thumbs = os.path.join(WORK_DIR, IMAGE_ROOT_DIR, THUMBS_DIR) |
|
if args.rebuild_all and os.path.exists(thumbs): |
|
print(f"Removing {thumbs}") |
|
shutil.rmtree(thumbs) |
|
|
|
# The very first time or in case of a rebuild all |
|
if not os.path.exists(thumbs): |
|
log(f"Creating {thumbs}") |
|
os.makedirs(thumbs) |
|
|
|
# Reset always |
|
svgs = os.path.join(WORK_DIR, IMAGE_ROOT_DIR, SVGS_DIR) |
|
if os.path.isdir(svgs): |
|
log(f"Clearing {svgs}") |
|
shutil.rmtree(svgs) |
|
os.makedirs(svgs) |
|
|
|
# Reset always |
|
spot = os.path.join(WORK_DIR, SPOT_DIR) |
|
if os.path.isdir(spot): |
|
log(f"Clearing {spot}") |
|
shutil.rmtree(spot) |
|
os.makedirs(spot) |
|
|
|
# Rebuild all: move existing original images to dropin |
|
if args.rebuild_all: |
|
|
|
p = re.compile('\d{8}.*\.(jpg|jpeg)', re.IGNORECASE) |
|
|
|
old_files = [f for f in os.listdir(IMAGES_DIR) |
|
if os.path.isfile(os.path.join(IMAGES_DIR, f)) and |
|
p.match(f)] |
|
|
|
if len(old_files) == 0: |
|
print(f"No files im {IMAGES_DIR}") |
|
else: |
|
print(f"Moving {len(old_files)} files from {IMAGES_DIR} to {DROPIN_DIR}", end="") |
|
|
|
for file in old_files: |
|
shutil.move(os.path.join(IMAGES_DIR, file), os.path.join(DROPIN_DIR, file)) |
|
|
|
log("..done") |
|
|
|
log("Initialization done") |
|
|
|
|
|
def exit_on_error(message): |
|
print(f"ERROR {message}") |
|
exit(1) |
|
|
|
|
|
def exit_with_good_bye(message): |
|
print(f"{message} --- Good bye ---") |
|
exit(0) |
|
|
|
|
|
def warn(message): |
|
print(f"WARN {message}") |
|
|
|
|
|
def do_it(): |
|
|
|
init_work() |
|
process_images() |
|
create_bundles() |
|
create_svgs() |
|
create_banner_svg() |
|
create_spots() |
|
activate() |
|
|
|
print("Congratulations! Generation done.") |
|
|
|
|
|
def parse_args(): |
|
|
|
global args |
|
|
|
parser = argparse.ArgumentParser(description="Update drs by adding new images and create spot files. " + |
|
"This script must be called from its directory 'redaktion'. Usage example: Add new images into ../../dropin directory and call without parameters. This images will be moved to ../../images. ") |
|
|
|
parser.add_argument("-v", "--verbose", action="store_true", |
|
help="Increase output verbosity.") |
|
|
|
parser.add_argument("-a", "--rebuild_all", action="store_true", |
|
help="Process images in /images in additional to the /dropin images") |
|
|
|
parser.add_argument("-l", "--location_mockup", action="store_true", |
|
help="Supress cost intensive location service. Mock all locations. Just for developer purposes.") |
|
|
|
args = parser.parse_args() |
|
|
|
|
|
if __name__ == '__main__': |
|
parse_args() |
|
do_it()
|
|
|