import { yardGeoJSON } from "./data/yard.js"; const TILE_MAX_NATIVE_ZOOM = 19; const MAP_MAX_ZOOM = 24; const map = L.map("map", { zoomControl: false, maxZoom: MAP_MAX_ZOOM, }); L.control .zoom({ position: "bottomright", }) .addTo(map); L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", { maxNativeZoom: TILE_MAX_NATIVE_ZOOM, maxZoom: MAP_MAX_ZOOM, attribution: '© OpenStreetMap', }).addTo(map); const geoJsonLayer = L.geoJSON(yardGeoJSON, { pointToLayer(feature, latlng) { const style = feature.properties.style; return L.circleMarker(latlng, { radius: style.radius ?? 6, color: style.color ?? "#333333", fillColor: style.fillColor ?? style.color ?? "#333333", fillOpacity: style.fillOpacity ?? 0.8, weight: style.weight ?? 2, }); }, style(feature) { return feature.properties.style; }, onEachFeature(feature, layer) { layer.bindPopup(buildPopupContent(feature.properties)); }, }).addTo(map); map.fitBounds(geoJsonLayer.getBounds().pad(0.2), { maxZoom: MAP_MAX_ZOOM }); const legend = document.getElementById("legend"); const legendEntries = summarizeTypes(yardGeoJSON.features); legend.replaceChildren( ...legendEntries.map((entry) => { const row = document.createElement("div"); row.className = "legend-item"; const swatch = document.createElement("span"); swatch.className = "legend-swatch"; swatch.style.background = entry.color; const label = document.createElement("span"); label.textContent = `${entry.label} (${entry.count})`; row.append(swatch, label); return row; }), ); function summarizeTypes(features) { const summary = new Map(); for (const feature of features) { const key = feature.properties.kind; const existing = summary.get(key) ?? { label: feature.properties.label, count: 0, color: feature.properties.style.color ?? "#333333", }; existing.count += 1; summary.set(key, existing); } return [...summary.values()]; } function buildPopupContent(properties) { const { label, name, details, parentId, groupIds } = properties; const entries = Object.entries(details ?? {}).filter(([, value]) => value !== null && value !== undefined); const relationshipEntries = []; if (parentId) { relationshipEntries.push(["parentFeature", parentId]); } if (groupIds?.length) { relationshipEntries.push(["groups", groupIds.join(", ")]); } const title = `${escapeHtml(name)}`; const subtitle = name === label ? "" : `
${escapeHtml(label)}
`; const allEntries = [...entries, ...relationshipEntries]; if (allEntries.length === 0) { return `${title}${subtitle}`; } const detailRows = allEntries .map(([key, value]) => `
${escapeHtml(formatDetailLabel(key))}: ${escapeHtml(formatDetailValue(key, value))}
`) .join(""); return `${title}${subtitle}
${detailRows}
`; } function formatDetailLabel(key) { const labels = { canopyDiameterMeters: "Canopy diameter", heightMeters: "Height", plantedDate: "Planted", species: "Species", fenceType: "Fence type", note: "Note", parentFeature: "Parent", groups: "Groups", }; return labels[key] ?? key; } function formatDetailValue(key, value) { if (key === "canopyDiameterMeters" || key === "heightMeters") { return `${value} m`; } return String(value); } function escapeHtml(value) { return String(value) .replaceAll("&", "&") .replaceAll("<", "<") .replaceAll(">", ">") .replaceAll('"', """) .replaceAll("'", "'"); }