import { yardGeoJSON } from "./data/yard.js"; import { resolveFeatureLinks, resolveFeatureStyle } from "./lib/feature-types.js"; const TILE_MAX_NATIVE_ZOOM = 19; const MAP_MAX_ZOOM = 24; const map = L.map("map", { zoomControl: false, maxZoom: MAP_MAX_ZOOM, }); const meterScaledIconMarkers = []; 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); L.geoJSON(yardGeoJSON, { pointToLayer(feature, latlng) { const style = getStyle(feature); const options = { color: style.color ?? "#333333", fillColor: style.fillColor ?? style.color ?? "#333333", fillOpacity: style.fillOpacity ?? 0.8, weight: style.weight ?? 2, }; if (style.iconUrl) { const marker = L.marker(latlng, { icon: buildIcon(style, latlng), }); if (typeof style.iconDiameterMeters === "number") { meterScaledIconMarkers.push({ marker, style, latlng }); } return marker; } if (typeof style.radiusMeters === "number") { return L.circle(latlng, { ...options, radius: style.radiusMeters, }); } return L.circleMarker(latlng, { ...options, radius: style.radius ?? 6, }); }, style(feature) { if (feature.geometry.type === "Point") { return getPathStyle(feature); } return getStyle(feature); }, onEachFeature(feature, layer) { layer.bindPopup(buildPopupContent(feature.properties)); }, }).addTo(map); map.fitBounds(getGeoJSONBounds(yardGeoJSON).pad(0.2), { maxZoom: MAP_MAX_ZOOM }); updateMeterScaledIcons(); map.on("zoomend", updateMeterScaledIcons); 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 style = getStyle(feature); const existing = summary.get(key) ?? { label: feature.properties.label, count: 0, color: style.color ?? "#333333", }; existing.count += 1; summary.set(key, existing); } return [...summary.values()]; } function getStyle(feature) { return resolveFeatureStyle(feature.properties.kind, feature.properties.details); } function getPathStyle(feature) { const { iconAnchor, iconDiameterMeters, iconSize, iconUrl, popupAnchor, radius, radiusMeters, ...style } = getStyle(feature); return style; } function buildIcon(style, latlng) { const iconSize = getIconSize(style, latlng); return L.icon({ iconUrl: style.iconUrl, iconSize, iconAnchor: [iconSize[0] / 2, iconSize[1] / 2], popupAnchor: [0, -iconSize[1] / 2], }); } function updateMeterScaledIcons() { for (const { marker, style, latlng } of meterScaledIconMarkers) { marker.setIcon(buildIcon(style, latlng)); } } function getIconSize(style, latlng) { if (typeof style.iconDiameterMeters !== "number") { return style.iconSize ?? [24, 24]; } if (typeof map.getZoom() !== "number") { return style.iconSize ?? [24, 24]; } const diameterPixels = style.iconDiameterMeters / getMetersPerPixel(latlng.lat); return [diameterPixels, diameterPixels]; } function getMetersPerPixel(lat) { return (156543.03392 * Math.cos((lat * Math.PI) / 180)) / 2 ** map.getZoom(); } function getGeoJSONBounds(collection) { const bounds = L.latLngBounds(); for (const feature of collection.features) { extendBounds(bounds, feature.geometry.coordinates); } return bounds; } function extendBounds(bounds, coordinates) { if (typeof coordinates[0] === "number") { const [lon, lat] = coordinates; bounds.extend([lat, lon]); return; } for (const coordinate of coordinates) { extendBounds(bounds, coordinate); } } function buildPopupContent(properties) { const { kind, label, name, startDate, endDate, details, parentId, groupIds } = properties; const entries = Object.entries(details ?? {}).filter( ([key, value]) => value !== null && value !== undefined && !key.endsWith("Url"), ); const lifecycleEntries = []; const relationshipEntries = []; const linkEntries = resolveFeatureLinks(kind, details); if (startDate) { lifecycleEntries.push(["startDate", startDate]); } if (endDate) { lifecycleEntries.push(["endDate", endDate]); } if (parentId) { relationshipEntries.push(["parentFeature", parentId]); } if (groupIds?.length) { relationshipEntries.push(["groups", groupIds.join(", ")]); } const title = `${escapeHtml(name)}`; const subtitle = name === label ? "" : `