Many improvements

This commit is contained in:
2026-06-26 15:48:12 -05:00
parent 2a52e0546e
commit c5725a0259
22 changed files with 1392 additions and 97 deletions
+150 -13
View File
@@ -1,4 +1,5 @@
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;
@@ -7,6 +8,7 @@ const map = L.map("map", {
zoomControl: false,
maxZoom: MAP_MAX_ZOOM,
});
const meterScaledIconMarkers = [];
L.control
.zoom({
@@ -20,26 +22,55 @@ L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>',
}).addTo(map);
const geoJsonLayer = L.geoJSON(yardGeoJSON, {
L.geoJSON(yardGeoJSON, {
pointToLayer(feature, latlng) {
const style = feature.properties.style;
return L.circleMarker(latlng, {
radius: style.radius ?? 6,
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) {
return feature.properties.style;
if (feature.geometry.type === "Point") {
return getPathStyle(feature);
}
return getStyle(feature);
},
onEachFeature(feature, layer) {
layer.bindPopup(buildPopupContent(feature.properties));
},
}).addTo(map);
map.fitBounds(geoJsonLayer.getBounds().pad(0.2), { maxZoom: MAP_MAX_ZOOM });
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);
@@ -66,10 +97,11 @@ function summarizeTypes(features) {
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: feature.properties.style.color ?? "#333333",
color: style.color ?? "#333333",
};
existing.count += 1;
@@ -79,10 +111,96 @@ function summarizeTypes(features) {
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 { label, name, details, parentId, groupIds } = properties;
const entries = Object.entries(details ?? {}).filter(([, value]) => value !== null && value !== undefined);
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]);
@@ -94,16 +212,19 @@ function buildPopupContent(properties) {
const title = `<strong>${escapeHtml(name)}</strong>`;
const subtitle = name === label ? "" : `<div>${escapeHtml(label)}</div>`;
const allEntries = [...entries, ...relationshipEntries];
if (allEntries.length === 0) {
const allEntries = [...lifecycleEntries, ...entries, ...relationshipEntries];
if (allEntries.length === 0 && linkEntries.length === 0) {
return `${title}${subtitle}`;
}
const detailRows = allEntries
.map(([key, value]) => `<div><span>${escapeHtml(formatDetailLabel(key))}:</span> ${escapeHtml(formatDetailValue(key, value))}</div>`)
.join("");
const linkRows = linkEntries
.map((link) => `<div><span>${escapeHtml(link.label)}:</span> <a href="${escapeHtml(link.url)}" target="_blank" rel="noreferrer">${escapeHtml(link.url)}</a></div>`)
.join("");
return `${title}${subtitle}<div>${detailRows}</div>`;
return `${title}${subtitle}<div>${detailRows}${linkRows}</div>`;
}
function formatDetailLabel(key) {
@@ -111,8 +232,20 @@ function formatDetailLabel(key) {
canopyDiameterMeters: "Canopy diameter",
heightMeters: "Height",
plantedDate: "Planted",
startDate: "Start",
endDate: "End",
commonName: "Common name",
genus: "Genus",
cultivar: "Cultivar",
bloomColor: "Bloom color",
bloomSeason: "Bloom season",
daylilyDatabaseId: "Daylily database ID",
daylilyDatabaseUrl: "Daylily database URL",
species: "Species",
fenceType: "Fence type",
axis: "Axis",
offsetFeet: "Offset",
spacingFeet: "Spacing",
note: "Note",
parentFeature: "Parent",
groups: "Groups",
@@ -126,6 +259,10 @@ function formatDetailValue(key, value) {
return `${value} m`;
}
if (key === "offsetFeet" || key === "spacingFeet") {
return `${value} ft`;
}
return String(value);
}
@@ -136,4 +273,4 @@ function escapeHtml(value) {
.replaceAll(">", "&gt;")
.replaceAll('"', "&quot;")
.replaceAll("'", "&#39;");
}
}