glam/scripts/generate_nde_map.py

266 lines
8.8 KiB
Python

#!/usr/bin/env python3
"""
Generate an interactive map of NDE heritage institutions.
Uses Folium (Leaflet.js) to create an HTML map with markers.
Usage:
python scripts/generate_nde_map.py
Output:
data/nde/exports/nde_map.html
"""
import json
import yaml
from pathlib import Path
from typing import Dict, List
import logging
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)
# Paths
PROJECT_ROOT = Path(__file__).parent.parent
ENTRIES_DIR = PROJECT_ROOT / "data" / "nde" / "enriched" / "entries"
EXPORTS_DIR = PROJECT_ROOT / "data" / "nde" / "exports"
# Institution type colors
TYPE_COLORS = {
"M": "#e74c3c", # Museum - red
"A": "#3498db", # Archive - blue
"L": "#2ecc71", # Library - green
"S": "#9b59b6", # Society - purple
"O": "#f39c12", # Official - orange
"R": "#1abc9c", # Research - teal
"D": "#34495e", # Digital - dark gray
"F": "#95a5a6", # Features - gray
"N": "#e91e63", # NGO - pink
"B": "#4caf50", # Botanical - green
"E": "#ff9800", # Education - amber
"I": "#673ab7", # Intangible - deep purple
"C": "#795548", # Corporation - brown
"H": "#607d8b", # Holy sites - blue gray
"T": "#ff5722", # Taste/smell - deep orange
"G": "#00bcd4", # Gallery - cyan
}
TYPE_NAMES = {
"M": "Museum",
"A": "Archive",
"L": "Library",
"S": "Society",
"O": "Official",
"R": "Research",
"D": "Digital",
"F": "Features",
"N": "NGO",
"B": "Botanical",
"E": "Education",
"I": "Intangible",
"C": "Corporation",
"H": "Holy sites",
"T": "Taste/smell",
"G": "Gallery",
}
def load_entries_with_coordinates() -> List[Dict]:
"""Load entries that have coordinates."""
entries = []
for yaml_file in sorted(ENTRIES_DIR.glob("*.yaml")):
try:
with open(yaml_file, "r", encoding="utf-8") as f:
entry = yaml.safe_load(f)
if entry and entry.get("wikidata_enrichment"):
coords = entry["wikidata_enrichment"].get("wikidata_coordinates", {})
if coords.get("latitude") and coords.get("longitude"):
entries.append(entry)
except Exception as e:
pass
return entries
def generate_html_map(entries: List[Dict]) -> str:
"""Generate an HTML file with a Leaflet map."""
# Prepare markers data
markers = []
for entry in entries:
original = entry.get("original_entry", {})
wikidata = entry.get("wikidata_enrichment", {})
coords = wikidata.get("wikidata_coordinates", {})
inst_types = original.get("type", ["M"])
primary_type = inst_types[0] if inst_types else "M"
name = original.get("organisatie", "Unknown")
city = original.get("plaatsnaam_bezoekadres", "")
website = original.get("webadres_organisatie", "")
wikidata_id = original.get("wikidata_id", "")
description = wikidata.get("wikidata_description_nl") or wikidata.get("wikidata_description_en", "")
markers.append({
"lat": coords["latitude"],
"lon": coords["longitude"],
"name": name,
"city": city,
"type": primary_type,
"type_name": TYPE_NAMES.get(primary_type, "Other"),
"color": TYPE_COLORS.get(primary_type, "#999999"),
"website": website,
"wikidata_id": wikidata_id,
"description": description[:200] + "..." if len(description) > 200 else description,
})
# Generate HTML
html = f'''<!DOCTYPE html>
<html>
<head>
<title>NDE Heritage Institutions Map</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
<link rel="stylesheet" href="https://unpkg.com/leaflet.markercluster@1.4.1/dist/MarkerCluster.css" />
<link rel="stylesheet" href="https://unpkg.com/leaflet.markercluster@1.4.1/dist/MarkerCluster.Default.css" />
<style>
body {{ margin: 0; padding: 0; font-family: Arial, sans-serif; }}
#map {{ position: absolute; top: 60px; bottom: 0; width: 100%; }}
#header {{
position: absolute;
top: 0;
left: 0;
right: 0;
height: 60px;
background: #2c3e50;
color: white;
display: flex;
align-items: center;
padding: 0 20px;
z-index: 1000;
}}
#header h1 {{ margin: 0; font-size: 20px; flex-grow: 1; }}
#header .stats {{ font-size: 14px; color: #bdc3c7; }}
.legend {{
position: absolute;
bottom: 30px;
right: 10px;
background: white;
padding: 10px;
border-radius: 5px;
box-shadow: 0 2px 5px rgba(0,0,0,0.3);
z-index: 1000;
max-height: 300px;
overflow-y: auto;
}}
.legend h4 {{ margin: 0 0 10px 0; }}
.legend-item {{
display: flex;
align-items: center;
margin: 5px 0;
}}
.legend-color {{
width: 20px;
height: 20px;
border-radius: 50%;
margin-right: 8px;
}}
.popup-content h3 {{ margin: 0 0 10px 0; }}
.popup-content p {{ margin: 5px 0; }}
.popup-content a {{ color: #3498db; }}
.type-badge {{
display: inline-block;
padding: 2px 8px;
border-radius: 3px;
color: white;
font-size: 12px;
margin-bottom: 5px;
}}
</style>
</head>
<body>
<div id="header">
<h1>NDE Heritage Institutions Map</h1>
<div class="stats">{len(markers)} institutions with coordinates</div>
</div>
<div id="map"></div>
<div class="legend">
<h4>Institution Types</h4>
{"".join(f'<div class="legend-item"><div class="legend-color" style="background:{color}"></div>{TYPE_NAMES.get(t, t)}</div>' for t, color in TYPE_COLORS.items())}
</div>
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<script src="https://unpkg.com/leaflet.markercluster@1.4.1/dist/leaflet.markercluster.js"></script>
<script>
// Initialize map centered on Netherlands
var map = L.map('map').setView([52.1, 5.3], 8);
// Add OpenStreetMap tiles
L.tileLayer('https://{{s}}.tile.openstreetmap.org/{{z}}/{{x}}/{{y}}.png', {{
attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
}}).addTo(map);
// Marker cluster group
var markers = L.markerClusterGroup({{
maxClusterRadius: 50,
spiderfyOnMaxZoom: true,
showCoverageOnHover: false,
zoomToBoundsOnClick: true
}});
// Institution data
var institutions = {json.dumps(markers)};
// Add markers
institutions.forEach(function(inst) {{
var marker = L.circleMarker([inst.lat, inst.lon], {{
radius: 8,
fillColor: inst.color,
color: '#fff',
weight: 2,
opacity: 1,
fillOpacity: 0.8
}});
var popupContent = '<div class="popup-content">' +
'<span class="type-badge" style="background:' + inst.color + '">' + inst.type_name + '</span>' +
'<h3>' + inst.name + '</h3>' +
(inst.city ? '<p><strong>City:</strong> ' + inst.city + '</p>' : '') +
(inst.description ? '<p>' + inst.description + '</p>' : '') +
(inst.website ? '<p><a href="' + inst.website + '" target="_blank">Website</a></p>' : '') +
(inst.wikidata_id ? '<p><a href="https://www.wikidata.org/wiki/' + inst.wikidata_id + '" target="_blank">Wikidata</a></p>' : '') +
'</div>';
marker.bindPopup(popupContent);
markers.addLayer(marker);
}});
map.addLayer(markers);
</script>
</body>
</html>'''
return html
def main():
"""Main function."""
logger.info("Loading entries with coordinates...")
entries = load_entries_with_coordinates()
logger.info(f"Found {len(entries)} entries with coordinates")
logger.info("Generating map...")
html = generate_html_map(entries)
# Save HTML
EXPORTS_DIR.mkdir(parents=True, exist_ok=True)
map_path = EXPORTS_DIR / "nde_map.html"
with open(map_path, "w", encoding="utf-8") as f:
f.write(html)
logger.info(f"Map saved to: {map_path}")
print(f"\nOpen in browser: file://{map_path}")
if __name__ == "__main__":
main()