Compare commits

...

3 commits

Author SHA1 Message Date
Frank Adaemmer fa8ac8c9d8 clean up html 2023-05-20 23:22:41 +02:00
Frank Adaemmer 36a3400e5d generate and load weeks 2023-05-20 23:19:11 +02:00
Frank Adaemmer 988c2fe898 start readme 2023-05-20 23:17:17 +02:00
4 changed files with 237 additions and 128 deletions

77
README.md Normal file
View file

@ -0,0 +1,77 @@
# Mappersons - Visualize People on a World Map
Mappersons is a web app that allows you to visualize pictures of people on a world map along with their respective tasks and locations. With its intuitive slider feature, users can easily navigate through different calendar weeks to see how locations change over time.
![Mapperson](screenshot.png)
# Usage
To use Mappersons, follow these steps:
1. Place square images of people in the `persons` folder. Each person should have a unique filename (e.g. `jane.jpg`, `bob.jpg`, etc.).
2. Create a `persons.yml` file according section persons.yml
3. Create a `locations.yml` file according section locations.yml
4. Run the `python mappersons.py` script to render the map.
5. Serve the output with `python -m http.server 8080` to make it accessible through a web browser.
## persons.yml
### Structure
This YAML file has a structure that includes the following fields:
- `name`: The name of the person or entity.
- `image`: The image associated with the person or entity.
- `weeks`: A list of weeks, each containing:
- `location`: The location where tasks were performed. Identically to locations.yml
- `tasks`: A list of tasks performed during that week at that location.
### Example File Content
```yaml
---
- name: Lisa Lurch
image: lurch_l.jpg
weeks:
21/2023:
- location: Hamburg
tasks:
- Test new Feature
22/2023:
- location: Hamburg
tasks:
- Maintenance NSP
- name: Walter Wiesel
image: wiesel_w.jpg
weeks:
21/2023:
- location: Teisnach
tasks:
- Wrap up Ship 1
22/2023:
- location: Hamburg
tasks:
- Backoffice Ship 1
- Write report
```
## locations.yml
This file contains a list of locations with their corresponding latitude and longitude coordinates.
### Structure
The structure of this YAML file is as follows:
```yaml
- name: [Name of the location]
lat: [Latitude in decimal degrees]
lng: [Longitude in decimal degrees]
```
### Example File Content
```yaml
---
- name: Hamburg
lat: 53.64286
lng: 9.9753
- name: La Spezia
lat: 44.1064
lng: 9.8439
- name: Teisnach
lat: 49.0301
lng: 12.998
```

View file

@ -1,128 +1,114 @@
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head> <head>
<title>World Map with OpenStreetMap</title> <title>Mappersons</title>
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.3/dist/leaflet.css" <link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.3/dist/leaflet.css"
integrity="sha256-kLaT2GOSpHechhsozzB+flnD+zUyjE2LlfWPgU04xyI=" integrity="sha256-kLaT2GOSpHechhsozzB+flnD+zUyjE2LlfWPgU04xyI=" crossorigin="" />
crossorigin=""/> <link href="jquery-ui.css" rel="stylesheet">
<link href="jquery-ui.css" rel="stylesheet">
<style> <style>
#map { #map {
height: 100vh; height: 90vh;
}
#slider {
position: relative;
width: 100%;
margin-bottom:1em;
}
#custom-handle {
width: 8em;
height: 1.6em;
top: 50%;
margin-top: -.8em;
text-align: center;
line-height: 1.6em;
}
</style>
<!-- Make sure you put this AFTER Leaflet's CSS -->
<script src="https://unpkg.com/leaflet@1.9.3/dist/leaflet.js"
integrity="sha256-WBkoXOwTeyKclOHuWtc+i2uENFpDZ9YPdf5Hf+D7ewM="
crossorigin=""></script>
</head>
<body>
<div id="slider"> <div id="custom-handle" class="ui-slider-handle"></div></div>
<div id="map"></div>
<script>
const map = L.map('map').setView([51.14027, 10.45863], 7);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: 'Map data &copy; <a href="https://www.openstreetmap.org/">OpenStreetMap</a> contributors, ' +
'<a href="https://creativecommons.org/licenses/by-sa/2.0/">CC-BY-SA</a>, Imagery © <a href="https://www.mapbox.com/">Mapbox</a>',
maxZoom: 18,
id: 'mapbox/streets-v11',
tileSize: 512,
zoomOffset: -1
}).addTo(map);
const locations = [
{
"city": "Hamburg",
"lat": "53.64286",
"lng": "9.9753",
"icon": "test5.png",
"anchor": [101,101]
},
// {
// "city": "Kiel",
// "lat": "54.3297",
// "lng": "10.1435"
// },
// {
// "city": "München",
// "lat": "48.1341",
// "lng": "11.5674",
// "icon": "test8.png",
// "anchor": [144,144]
// },
{
"city": "La Spezia",
"lat": "44.1064",
"lng": "9.8439",
"icon": "test8.png",
"anchor": [144,144]
},
{
"city": "Teisnach",
"lat": "49.0301",
"lng": "12.998",
"icon": "test13.png",
"anchor": [144,144]
}
];
const markers = locations.map(location => {
return L.marker([location.lat, location.lng], {icon: L.icon({
iconUrl: location.icon,
iconAnchor: location.anchor
})}).addTo(map);
});
</script>
<script src="external/jquery/jquery.js"></script>
<script src="jquery-ui.js"></script>
<script>
Date.prototype.getWeek = function() {
var onejan = new Date(this.getFullYear(), 0, 1);
return Math.ceil((((this - onejan) / 86400000) + onejan.getDay() + 1) / 7);
} }
// Set initial date to current calendar week and year
// Initialize slider with current calendar week and year
let now = new Date();
let year = now.getFullYear(); // get the full four-digit year
let week = now.getWeek(); // get the week as a number, where 0 represents the first week of the year
console.log(`The current week of ${year} is ${week}`);
var handle = $( "#custom-handle" ); #slider {
position: relative;
width: 100%;
margin-bottom: 1em;
}
#custom-handle {
width: 8em;
height: 1.6em;
top: 50%;
margin-top: -.8em;
text-align: center;
line-height: 1.6em;
}
</style>
<!-- Make sure you put this AFTER Leaflet's CSS -->
<script src="https://unpkg.com/leaflet@1.9.3/dist/leaflet.js"
integrity="sha256-WBkoXOwTeyKclOHuWtc+i2uENFpDZ9YPdf5Hf+D7ewM=" crossorigin=""></script>
</head>
<body>
<div id="slider">
<div id="custom-handle" class="ui-slider-handle"></div>
</div>
<div id="map"></div>
<script>
const map = L.map('map').setView([51.14027, 10.45863], 7);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: 'Map data &copy; <a href="https://www.openstreetmap.org/">OpenStreetMap</a> contributors, ' +
'<a href="https://creativecommons.org/licenses/by-sa/2.0/">CC-BY-SA</a>, Imagery &copy <a href="https://www.mapbox.com/">Mapbox</a>',
maxZoom: 18,
id: 'mapbox/streets-v11',
tileSize: 512,
zoomOffset: -1
}).addTo(map);
var markerLayer = L.layerGroup();
function loadWeek(locations) {
locations.map(location => {
return L.marker([location.lat, location.lng], {
icon: L.icon({
iconUrl: location.icon,
iconAnchor: location.anchor
})
})
.bindPopup(location.popup)
.addTo(markerLayer);
});
map.addLayer(markerLayer)
}
</script>
<script src="external/jquery/jquery.js"></script>
<script src="jquery-ui.js"></script>
<script>
Date.prototype.getWeek = function () {
var onejan = new Date(this.getFullYear(), 0, 1);
return Math.ceil((((this - onejan) / 86400000) + onejan.getDay() + 1) / 7);
}
// Set initial date to current calendar week and year
// Initialize slider with current calendar week and year
let now = new Date();
let year = now.getFullYear(); // get the full four-digit year
let week = now.getWeek(); // get the week as a number, where 0 represents the first week of the year
console.log(`The current week of ${year} is ${week}`);
var handle = $("#custom-handle");
$("#slider").slider({ $("#slider").slider({
value: week, value: week,
min: week - 4, min: week - 4,
max: week + 12, max: week + 12,
step: 1, step: 1,
create: function() { create: function () {
handle.text( `KW ${week}/${year}`); handle.text(`KW ${week}/${year}`);
$.ajaxSetup({ cache: false });
$.getJSON(`cw_${week}_${year}.json`, function (data) {
loadWeek(data);
});
}, },
slide: function( event, ui ) { slide: function (event, ui) {
// ToDo: handle year change
var curweek = ui.value; var curweek = ui.value;
var curyear = year; var curyear = year;
if (map.hasLayer(markerLayer)) {
handle.text( `KW ${curweek}/${curyear}`); markerLayer.clearLayers();
}
handle.text(`KW ${curweek}/${curyear}`);
$.ajaxSetup({ cache: false });
$.getJSON(`cw_${curweek}_${curyear}.json`, function (data) {
loadWeek(data);
});
} }
}); });
</script> </script>
</body> </body>
</html> </html>

View file

@ -1,9 +1,9 @@
import math import math
import json
import yaml
from PIL import Image, ImageDraw, ImageFilter from PIL import Image, ImageDraw, ImageFilter
from pathlib import Path from pathlib import Path
import numpy as np
import math
def circular_mask(img): def circular_mask(img):
center_x = int(img.width / 2) center_x = int(img.width / 2)
@ -17,7 +17,7 @@ def circular_mask(img):
return img return img
def arrange_imager_in_circle(images, spacing=5, min_radius=0): def arrange_images_in_circle(images, spacing=5, min_radius=0):
d = images[0].size[0] d = images[0].size[0]
dis = d + spacing dis = d + spacing
@ -51,7 +51,7 @@ def arrange_imager_in_circle(images, spacing=5, min_radius=0):
return masterImage return masterImage
def create_circle(image_paths, output_path): def create_circle(image_paths):
# Resize all images to 64x64 pixels # Resize all images to 64x64 pixels
shrunk_images = [Image.open(path).resize((64, 64)) for path in image_paths] shrunk_images = [Image.open(path).resize((64, 64)) for path in image_paths]
@ -63,26 +63,72 @@ def create_circle(image_paths, output_path):
return return
if len(circular_images) < 7: if len(circular_images) < 7:
output_img = arrange_imager_in_circle(circular_images) output_img = arrange_images_in_circle(circular_images)
ow, oh = output_img.size ow, oh = output_img.size
else: else:
inner_img = arrange_imager_in_circle(circular_images[:4]) inner_img = arrange_images_in_circle(circular_images[:4])
iw, ih = inner_img.size iw, ih = inner_img.size
mask = inner_img.split()[-1] mask = inner_img.split()[-1]
dr = circular_images[0].size[0]//2 + iw//2 dr = circular_images[0].size[0]//2 + iw//2
output_img = arrange_imager_in_circle(circular_images[4:],min_radius=dr) output_img = arrange_images_in_circle(circular_images[4:],min_radius=dr)
ow, oh = output_img.size ow, oh = output_img.size
output_img.paste(inner_img,((ow-iw)//2,(oh-ih)//2),mask) output_img.paste(inner_img,((ow-iw)//2,(oh-ih)//2),mask)
draw = ImageDraw.Draw(output_img) draw = ImageDraw.Draw(output_img)
dr = 3 dr = 3
draw.ellipse((ow//2-dr, oh//2-dr, ow//2+dr, oh//2+dr),fill=(255,0,0),outline=False) draw.ellipse((ow//2-dr, oh//2-dr, ow//2+dr, oh//2+dr),fill=(255,0,0),outline=False)
output_path = output_img.save(output_path) return output_img
def generateWeek(week, persons, locations, output_path=Path('.')):
loc = {}
for person in persons:
print(person)
if week in person['weeks'].keys():
cur_name = person['name']
cur_image = person['image']
for task in person['weeks'][week]:
cur_loc = task['location']
cur_tasks = task['tasks']
if cur_loc not in loc.keys():
loc[cur_loc] = {}
if cur_name not in loc[cur_loc].keys():
loc[cur_loc][cur_name] = {
'image': cur_image,
'tasks': []
}
loc[cur_loc][cur_name]['tasks'] += cur_tasks
week_json = []
w,y = week.split('/')
json_file = "cw_%s_%s.json" % (w,y)
for cur_loc, cur_persons in loc.items():
print(cur_loc)
print(cur_persons)
png_file = "%s_%s_%s.png" % (cur_loc.lower(),w,y)
[print(x) for x in cur_persons]
image_paths = [("persons/%s" % x['image']) for n,x in cur_persons.items()]
img = create_circle(image_paths)
img.save(output_path / Path(png_file))
popup = "<h2>%s</h2>" % cur_loc
for cur_per, cur_info in cur_persons.items():
popup += "<h3>%s</h3>" % cur_per
popup += "</br>".join(cur_info['tasks'])
l = [x for x in locations if x['name'] == cur_loc][0]
week_json.append({
"city": cur_loc,
"lat": l['lat'],
"lng": l['lng'],
"icon": png_file,
"anchor": [x//2 for x in img.size],
"popup": popup
})
print()
with open(output_path / Path(json_file),"w") as j:
json.dump(week_json, j)
if __name__ == "__main__": if __name__ == "__main__":
p = Path("test_images") with open("persons.yml","r") as fp:
image_paths = [str(x) for x in p.glob("*.jpg")]*2 persons = yaml.safe_load(fp)
for i in range(len(image_paths)): with open("locations.yml","r") as fp:
output_path = "test%i.png" % i locations = yaml.safe_load(fp)
create_circle(image_paths[:i+1], output_path) generateWeek('21/2023', persons, locations)
generateWeek('22/2023', persons, locations)

BIN
screenshot.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 421 KiB