Prototype

This commit is contained in:
2026-06-26 11:23:09 -05:00
commit 2a52e0546e
19 changed files with 1406 additions and 0 deletions
+139
View File
@@ -0,0 +1,139 @@
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: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>',
}).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 = `<strong>${escapeHtml(name)}</strong>`;
const subtitle = name === label ? "" : `<div>${escapeHtml(label)}</div>`;
const allEntries = [...entries, ...relationshipEntries];
if (allEntries.length === 0) {
return `${title}${subtitle}`;
}
const detailRows = allEntries
.map(([key, value]) => `<div><span>${escapeHtml(formatDetailLabel(key))}:</span> ${escapeHtml(formatDetailValue(key, value))}</div>`)
.join("");
return `${title}${subtitle}<div>${detailRows}</div>`;
}
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("&", "&amp;")
.replaceAll("<", "&lt;")
.replaceAll(">", "&gt;")
.replaceAll('"', "&quot;")
.replaceAll("'", "&#39;");
}