266 lines
8.8 KiB
Python
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: '© <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()
|