Many improvements
This commit is contained in:
+150
-13
@@ -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: '© <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(">", ">")
|
||||
.replaceAll('"', """)
|
||||
.replaceAll("'", "'");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user