Compare commits
3 commits
1845e30a1f
...
fa8ac8c9d8
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fa8ac8c9d8 | ||
|
|
36a3400e5d | ||
|
|
988c2fe898 |
77
README.md
Normal file
77
README.md
Normal 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.
|
||||||
|
|
||||||
|

|
||||||
|
# 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
|
||||||
|
```
|
||||||
216
index.html
216
index.html
|
|
@ -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 © <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 © <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);
|
||||||
|
|
||||||
|
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>
|
||||||
|
|
|
||||||
|
|
@ -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
BIN
screenshot.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 421 KiB |
Loading…
Reference in a new issue