Files
mcm-mfp/task3/fig1_carto.html
2026-01-20 01:55:46 +08:00

185 lines
6.1 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Task 3 Fig.1 (CartoDB + Leaflet)</title>
<link
rel="stylesheet"
href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"
crossorigin=""
/>
<style>
html,
body,
#map {
height: 100%;
margin: 0;
}
body {
background: #ffffff;
}
#map {
background: #ffffff;
}
.legend {
background: rgba(255, 255, 255, 0.96);
padding: 10px 12px;
border-radius: 8px;
box-shadow: 0 1px 10px rgba(0, 0, 0, 0.15);
font: 12px/1.3 system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, sans-serif;
border: 1px solid rgba(0, 0, 0, 0.15);
}
.legend .title {
font-weight: 700;
margin-bottom: 6px;
}
.legend .row {
display: flex;
justify-content: space-between;
gap: 12px;
white-space: nowrap;
}
.legend .swatch {
width: 12px;
height: 12px;
border-radius: 10px;
display: inline-block;
margin-right: 6px;
border: 1px solid rgba(255, 255, 255, 0.95);
vertical-align: -1px;
}
.legend .muted {
color: rgba(0, 0, 0, 0.65);
margin-top: 6px;
}
.legend .line {
height: 2px;
background: rgba(188, 108, 37, 0.75);
border-radius: 2px;
margin-top: 6px;
}
</style>
</head>
<body>
<div id="map"></div>
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js" crossorigin=""></script>
<script src="./fig1_points.js"></script>
<script>
const points = window.FIG1_POINTS || [];
const links = window.FIG1_LINKS || [];
if (!points.length) {
alert(
"Missing FIG1_POINTS: run `python task3/08_visualize.py` to generate `task3/fig1_points.js`, and ensure it is in the same directory as this HTML file."
);
}
const map = L.map("map", { preferCanvas: true });
// CartoDB basemap (Positron)
L.tileLayer("https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png", {
subdomains: "abcd",
maxZoom: 20,
attribution:
'&copy; <a href="https://www.openstreetmap.org/copyright">OSM</a> contributors &copy; <a href="https://carto.com/attributions">CARTO</a>',
}).addTo(map);
const byId = new Map(points.map((p) => [p.site_id, p]));
const muValues = points.map((p) => p.mu);
const muMin = Math.min(...muValues);
const muMax = Math.max(...muValues);
const COLORS = {
// warmer + higher contrast
paired: "#e76f51",
unpaired: "#6c757d",
link: "#bc6c25",
};
function clamp01(x) {
return Math.max(0, Math.min(1, x));
}
function lerp(a, b, t) {
return a + (b - a) * t;
}
function radiusForMu(mu) {
const t = clamp01((mu - muMin) / (muMax - muMin || 1));
return lerp(5, 20, t);
}
function colorForPaired(isPaired) {
return isPaired ? COLORS.paired : COLORS.unpaired;
}
const siteLayer = L.featureGroup();
for (const p of points) {
const marker = L.circleMarker([p.lat, p.lng], {
radius: radiusForMu(p.mu),
color: "rgba(255,255,255,0.96)",
weight: 1.35,
fillColor: colorForPaired(p.is_paired),
fillOpacity: 0.92,
});
marker.bindPopup(
`<div style="font: 13px/1.35 system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, sans-serif;">\n` +
`<div style="font-weight:700;margin-bottom:6px;">${p.site_id}. ${p.site_name}</div>` +
`<div><b>Mean (μ)</b>: ${p.mu.toFixed(1)}</div>` +
`<div><b>Std (σ)</b>: ${p.sigma.toFixed(1)}</div>` +
`<div><b>Annual visits (k)</b>: ${p.k}</div>` +
`<div><b>Paired</b>: ${p.is_paired ? "Yes" : "No"}</div>` +
`</div>`
);
siteLayer.addLayer(marker);
}
siteLayer.addTo(map);
const linkLayer = L.featureGroup();
for (const e of links) {
const a = byId.get(e.site_i_id);
const b = byId.get(e.site_j_id);
if (!a || !b) continue;
const line = L.polyline(
[
[a.lat, a.lng],
[b.lat, b.lng],
],
{
color: COLORS.link,
weight: 2.6,
opacity: 0.5,
}
);
line.bindPopup(
`<div style="font: 13px/1.35 system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, sans-serif;">\n` +
`<div style="font-weight:700;margin-bottom:6px;">Pair</div>` +
`<div>${e.site_i_id}. ${e.site_i_name}</div>` +
`<div>${e.site_j_id}. ${e.site_j_name}</div>` +
`<div><b>Distance</b>: ${e.distance.toFixed(2)} mi</div>` +
`</div>`
);
linkLayer.addLayer(line);
}
linkLayer.addTo(map);
const all = L.featureGroup([siteLayer, linkLayer]);
map.fitBounds(all.getBounds().pad(0.12));
const legend = L.control({ position: "bottomleft" });
legend.onAdd = function () {
const pairedCount = points.filter((p) => p.is_paired).length;
const linkCount = links.length;
const div = L.DomUtil.create("div", "legend");
div.innerHTML =
`<div class="title">Task 3 Fig.1 Pairing Map (Interactive)</div>` +
`<div class="row"><span><span class="swatch" style="background:${COLORS.paired}"></span>Paired sites</span><span>${pairedCount}</span></div>` +
`<div class="row"><span><span class="swatch" style="background:${COLORS.unpaired}"></span>Unpaired sites</span><span>${points.length - pairedCount}</span></div>` +
`<div class="muted">Marker size: mean demand μ (linear scale)</div>` +
`<div class="line"></div>` +
`<div class="muted">Links: ${linkCount} selected pairs (click for distance)</div>`;
return div;
};
legend.addTo(map);
</script>
</body>
</html>