@canopy-iiif/app 1.3.5 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/ui/dist/index.mjs CHANGED
@@ -2692,6 +2692,656 @@ function TimelinePoint() {
2692
2692
  return null;
2693
2693
  }
2694
2694
  TimelinePoint.displayName = "TimelinePoint";
2695
+
2696
+ // ui/src/content/map/Map.jsx
2697
+ import React34 from "react";
2698
+ var DEFAULT_TILE_LAYERS = [
2699
+ {
2700
+ name: "OpenStreetMap",
2701
+ url: "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png",
2702
+ attribution: "© OpenStreetMap contributors",
2703
+ maxZoom: 19
2704
+ }
2705
+ ];
2706
+ function resolveGlobalLeaflet() {
2707
+ try {
2708
+ if (typeof globalThis !== "undefined" && globalThis.L) return globalThis.L;
2709
+ } catch (_) {
2710
+ }
2711
+ try {
2712
+ if (typeof window !== "undefined" && window.L) return window.L;
2713
+ } catch (_) {
2714
+ }
2715
+ return null;
2716
+ }
2717
+ function waitForLeaflet(timeoutMs = 5e3) {
2718
+ const existing = resolveGlobalLeaflet();
2719
+ if (existing) return Promise.resolve(existing);
2720
+ let timer = null;
2721
+ return new Promise((resolve, reject) => {
2722
+ const deadline = Date.now() + timeoutMs;
2723
+ const onReady = () => {
2724
+ const resolved = resolveGlobalLeaflet();
2725
+ if (resolved) {
2726
+ cleanup();
2727
+ resolve(resolved);
2728
+ }
2729
+ };
2730
+ const poll = () => {
2731
+ const resolved = resolveGlobalLeaflet();
2732
+ if (resolved) {
2733
+ cleanup();
2734
+ resolve(resolved);
2735
+ return;
2736
+ }
2737
+ if (Date.now() > deadline) {
2738
+ cleanup();
2739
+ reject(new Error("Leaflet runtime not available"));
2740
+ return;
2741
+ }
2742
+ timer = setTimeout(poll, 50);
2743
+ };
2744
+ const cleanup = () => {
2745
+ if (timer) clearTimeout(timer);
2746
+ if (typeof document !== "undefined") {
2747
+ document.removeEventListener("canopy:leaflet-ready", onReady);
2748
+ }
2749
+ };
2750
+ if (typeof document !== "undefined") {
2751
+ document.addEventListener("canopy:leaflet-ready", onReady);
2752
+ }
2753
+ poll();
2754
+ });
2755
+ }
2756
+ function readBasePath2() {
2757
+ const normalize = (val) => {
2758
+ if (!val) return "";
2759
+ const raw = String(val).trim();
2760
+ if (!raw) return "";
2761
+ const withLead = raw.startsWith("/") ? raw : `/${raw}`;
2762
+ return withLead.replace(/\/+$/, "");
2763
+ };
2764
+ try {
2765
+ if (typeof window !== "undefined" && window.CANOPY_BASE_PATH != null) {
2766
+ const fromWindow = normalize(window.CANOPY_BASE_PATH);
2767
+ if (fromWindow) return fromWindow;
2768
+ }
2769
+ } catch (_) {
2770
+ }
2771
+ try {
2772
+ if (typeof globalThis !== "undefined" && globalThis.CANOPY_BASE_PATH != null) {
2773
+ const fromGlobal = normalize(globalThis.CANOPY_BASE_PATH);
2774
+ if (fromGlobal) return fromGlobal;
2775
+ }
2776
+ } catch (_) {
2777
+ }
2778
+ try {
2779
+ if (typeof process !== "undefined" && process.env && process.env.CANOPY_BASE_PATH) {
2780
+ const fromEnv = normalize(process.env.CANOPY_BASE_PATH);
2781
+ if (fromEnv) return fromEnv;
2782
+ }
2783
+ } catch (_) {
2784
+ }
2785
+ return "";
2786
+ }
2787
+ function withBasePath(href) {
2788
+ try {
2789
+ const raw = typeof href === "string" ? href.trim() : "";
2790
+ if (!raw) return raw;
2791
+ if (/^(?:[a-z][a-z0-9+.-]*:|\/\/|#)/i.test(raw)) return raw;
2792
+ if (!raw.startsWith("/")) return raw;
2793
+ const base = readBasePath2();
2794
+ if (!base || base === "/") return raw;
2795
+ if (raw === base || raw.startsWith(`${base}/`)) return raw;
2796
+ return `${base}${raw}`;
2797
+ } catch (_) {
2798
+ return href;
2799
+ }
2800
+ }
2801
+ function normalizeKey(value) {
2802
+ if (!value && value !== 0) return "";
2803
+ try {
2804
+ const str = String(value).trim();
2805
+ if (!str) return "";
2806
+ return str.replace(/\.html?$/i, "").replace(/\/+$/, "").toLowerCase();
2807
+ } catch (_) {
2808
+ return "";
2809
+ }
2810
+ }
2811
+ function createMarkerMap() {
2812
+ try {
2813
+ if (typeof globalThis !== "undefined" && typeof globalThis.Map === "function") {
2814
+ return new globalThis.Map();
2815
+ }
2816
+ } catch (_) {
2817
+ }
2818
+ try {
2819
+ if (typeof window !== "undefined" && typeof window.Map === "function") {
2820
+ return new window.Map();
2821
+ }
2822
+ } catch (_) {
2823
+ }
2824
+ const store = /* @__PURE__ */ Object.create(null);
2825
+ return {
2826
+ has(key) {
2827
+ return Object.prototype.hasOwnProperty.call(store, key);
2828
+ },
2829
+ get(key) {
2830
+ return store[key];
2831
+ },
2832
+ set(key, value) {
2833
+ store[key] = value;
2834
+ return this;
2835
+ }
2836
+ };
2837
+ }
2838
+ function readIiifType(resource) {
2839
+ if (!resource) return "";
2840
+ const raw = resource.type || resource["@type"];
2841
+ const list = Array.isArray(raw) ? raw : raw ? [raw] : [];
2842
+ const normalized = list.map((entry) => {
2843
+ try {
2844
+ return String(entry).toLowerCase();
2845
+ } catch (_) {
2846
+ return "";
2847
+ }
2848
+ }).filter(Boolean);
2849
+ if (normalized.some((value) => value.includes("manifest"))) return "manifest";
2850
+ if (normalized.some((value) => value.includes("collection"))) return "collection";
2851
+ return "";
2852
+ }
2853
+ function extractCollectionEntries(resource) {
2854
+ if (!resource || typeof resource !== "object") return [];
2855
+ const chunks = [];
2856
+ if (Array.isArray(resource.items)) chunks.push(resource.items);
2857
+ if (Array.isArray(resource.manifests)) chunks.push(resource.manifests);
2858
+ if (Array.isArray(resource.members)) chunks.push(resource.members);
2859
+ return chunks.flat();
2860
+ }
2861
+ function extractManifestKeysFromIiif(resource, fallback) {
2862
+ const fallbackKey = normalizeKey(fallback);
2863
+ if (!resource) return fallbackKey ? [fallbackKey] : [];
2864
+ const type = readIiifType(resource);
2865
+ if (type === "manifest") {
2866
+ const id = resource.id || resource["@id"] || fallback;
2867
+ const key = normalizeKey(id);
2868
+ return key ? [key] : fallbackKey ? [fallbackKey] : [];
2869
+ }
2870
+ if (type === "collection") {
2871
+ const seen = /* @__PURE__ */ new Set();
2872
+ const keys = [];
2873
+ const queue = extractCollectionEntries(resource).slice();
2874
+ while (queue.length) {
2875
+ const entry = queue.shift();
2876
+ if (!entry || typeof entry !== "object") continue;
2877
+ const entryType = readIiifType(entry);
2878
+ if (entryType === "manifest") {
2879
+ const manifestId = entry.id || entry["@id"];
2880
+ const key = normalizeKey(manifestId);
2881
+ if (key && !seen.has(key)) {
2882
+ seen.add(key);
2883
+ keys.push(key);
2884
+ }
2885
+ } else if (entryType === "collection") {
2886
+ queue.push(...extractCollectionEntries(entry));
2887
+ }
2888
+ }
2889
+ if (keys.length) return keys;
2890
+ }
2891
+ return fallbackKey ? [fallbackKey] : [];
2892
+ }
2893
+ function buildTileLayers(inputLayers, leaflet) {
2894
+ if (!leaflet) return [];
2895
+ const layers = Array.isArray(inputLayers) && inputLayers.length ? inputLayers : DEFAULT_TILE_LAYERS;
2896
+ return layers.map((entry) => {
2897
+ if (!entry || !entry.url) return null;
2898
+ const options = {};
2899
+ if (entry.attribution) options.attribution = entry.attribution;
2900
+ if (typeof entry.maxZoom === "number") options.maxZoom = entry.maxZoom;
2901
+ if (typeof entry.minZoom === "number") options.minZoom = entry.minZoom;
2902
+ if (entry.subdomains) options.subdomains = entry.subdomains;
2903
+ return {
2904
+ name: entry.name || entry.title || "Layer",
2905
+ layer: leaflet.tileLayer(entry.url, options)
2906
+ };
2907
+ }).filter(Boolean);
2908
+ }
2909
+ function buildMarkerIcon(marker, leaflet) {
2910
+ if (!leaflet) return null;
2911
+ const hasThumbnail = Boolean(marker && marker.thumbnail);
2912
+ const size = 56;
2913
+ const anchor = size / 2;
2914
+ const html = hasThumbnail ? `<div class="canopy-map__marker-thumb"><img src="${escapeHtml(
2915
+ marker.thumbnail
2916
+ )}" alt="" loading="lazy" /></div>` : '<span class="canopy-map__marker-solid"></span>';
2917
+ try {
2918
+ return leaflet.divIcon({
2919
+ className: "canopy-map__marker",
2920
+ iconSize: [size, size],
2921
+ iconAnchor: [anchor, anchor],
2922
+ popupAnchor: [0, -anchor + 10],
2923
+ html
2924
+ });
2925
+ } catch (_) {
2926
+ return null;
2927
+ }
2928
+ }
2929
+ function buildClusterOptions(leaflet) {
2930
+ if (!leaflet) return null;
2931
+ const size = 56;
2932
+ const anchor = size / 2;
2933
+ return {
2934
+ chunkedLoading: true,
2935
+ iconCreateFunction: (cluster) => {
2936
+ const count = cluster && typeof cluster.getChildCount === "function" ? cluster.getChildCount() : 0;
2937
+ return leaflet.divIcon({
2938
+ html: `<div class="canopy-map__cluster">${count}</div>`,
2939
+ className: "canopy-map__cluster-wrapper",
2940
+ iconSize: [size, size],
2941
+ iconAnchor: [anchor, anchor]
2942
+ });
2943
+ }
2944
+ };
2945
+ }
2946
+ function escapeHtml(value) {
2947
+ if (value == null) return "";
2948
+ return String(value).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
2949
+ }
2950
+ function renderPopup(marker) {
2951
+ if (!marker) return "";
2952
+ const title = marker.title || marker.manifestTitle || "";
2953
+ const summary = marker.summary || marker.manifestSummary || "";
2954
+ const href = marker.href ? withBasePath(marker.href) : "";
2955
+ const thumbnail = marker.thumbnail || "";
2956
+ const thumbWidth = marker.thumbnailWidth;
2957
+ const thumbHeight = marker.thumbnailHeight;
2958
+ const chunks = ['<div class="canopy-map__popup">'];
2959
+ if (thumbnail) {
2960
+ const sizeAttrs = [];
2961
+ if (typeof thumbWidth === "number" && thumbWidth > 0) sizeAttrs.push(`width="${thumbWidth}"`);
2962
+ if (typeof thumbHeight === "number" && thumbHeight > 0) sizeAttrs.push(`height="${thumbHeight}"`);
2963
+ chunks.push(
2964
+ `<div class="canopy-map__popup-media"><img src="${escapeHtml(thumbnail)}" alt="" loading="lazy" ${sizeAttrs.join(" ")} /></div>`
2965
+ );
2966
+ }
2967
+ chunks.push('<div class="canopy-map__popup-body">');
2968
+ if (title) {
2969
+ const heading = href ? `<a href="${escapeHtml(href)}" class="canopy-map__popup-title">${escapeHtml(title)}</a>` : `<span class="canopy-map__popup-title">${escapeHtml(title)}</span>`;
2970
+ chunks.push(heading);
2971
+ }
2972
+ if (summary) chunks.push(`<p class="canopy-map__popup-summary">${escapeHtml(summary)}</p>`);
2973
+ if (marker.detailsHtml) chunks.push(`<div class="canopy-map__popup-details">${marker.detailsHtml}</div>`);
2974
+ if (!summary && !marker.detailsHtml && href && !title) {
2975
+ chunks.push(`<a href="${escapeHtml(href)}" class="canopy-map__popup-link">View item</a>`);
2976
+ }
2977
+ chunks.push("</div></div>");
2978
+ return chunks.join("");
2979
+ }
2980
+ function normalizeCustomMarkers(points) {
2981
+ if (!Array.isArray(points)) return [];
2982
+ return points.map((point) => {
2983
+ if (!point) return null;
2984
+ const lat = Number(point.lat);
2985
+ const lng = Number(point.lng);
2986
+ if (!Number.isFinite(lat) || !Number.isFinite(lng)) return null;
2987
+ return {
2988
+ id: point.id || `${lat}-${lng}`,
2989
+ lat,
2990
+ lng,
2991
+ title: point.title || point.label || "",
2992
+ summary: point.summary || "",
2993
+ detailsHtml: point.detailsHtml || "",
2994
+ href: point.href || "",
2995
+ thumbnail: point.thumbnail || "",
2996
+ thumbnailWidth: point.thumbnailWidth,
2997
+ thumbnailHeight: point.thumbnailHeight,
2998
+ type: "custom"
2999
+ };
3000
+ }).filter(Boolean);
3001
+ }
3002
+ function extractNavMarkers(data, allowedKeys) {
3003
+ if (!data || !Array.isArray(data.manifests)) return [];
3004
+ const keys = allowedKeys instanceof Set ? allowedKeys : /* @__PURE__ */ new Set();
3005
+ if (!keys.size) return [];
3006
+ const markers = [];
3007
+ data.manifests.forEach((entry) => {
3008
+ if (!entry || !Array.isArray(entry.features)) return;
3009
+ const manifestKeys = new Set([
3010
+ normalizeKey(entry.id),
3011
+ normalizeKey(entry.href),
3012
+ normalizeKey(entry.slug)
3013
+ ].filter(Boolean));
3014
+ const hasMatch = Array.from(manifestKeys).some((key) => keys.has(key));
3015
+ if (!hasMatch) return;
3016
+ const matchKeys = Array.from(manifestKeys);
3017
+ entry.features.forEach((feature, index) => {
3018
+ if (!feature) return;
3019
+ const lat = Number(feature.lat);
3020
+ const lng = Number(feature.lng);
3021
+ if (!Number.isFinite(lat) || !Number.isFinite(lng)) return;
3022
+ const id = feature.id || `${entry.id || entry.slug || "feature"}-${index}`;
3023
+ markers.push({
3024
+ id,
3025
+ lat,
3026
+ lng,
3027
+ title: feature.label || entry.title || "",
3028
+ summary: feature.summary || entry.summary || "",
3029
+ href: entry.href || "",
3030
+ thumbnail: entry.thumbnail || "",
3031
+ thumbnailWidth: entry.thumbnailWidth,
3032
+ thumbnailHeight: entry.thumbnailHeight,
3033
+ manifestTitle: entry.title || "",
3034
+ manifestSummary: entry.summary || "",
3035
+ type: "navPlace",
3036
+ matchKeys
3037
+ });
3038
+ });
3039
+ });
3040
+ return markers;
3041
+ }
3042
+ function normalizeCenterInput(value) {
3043
+ var _a, _b, _c, _d, _e, _f;
3044
+ if (!value) return null;
3045
+ if (typeof value === "string") {
3046
+ const parts = value.split(/[,\s]+/).filter(Boolean);
3047
+ if (parts.length >= 2) {
3048
+ const lat = Number(parts[0]);
3049
+ const lng = Number(parts[1]);
3050
+ if (Number.isFinite(lat) && Number.isFinite(lng)) return { lat, lng };
3051
+ }
3052
+ return null;
3053
+ }
3054
+ if (typeof value === "object") {
3055
+ const lat = Number((_b = (_a = value.lat) != null ? _a : value.latitude) != null ? _b : value.y);
3056
+ const lng = Number((_f = (_e = (_d = (_c = value.lng) != null ? _c : value.lon) != null ? _d : value.long) != null ? _e : value.longitude) != null ? _f : value.x);
3057
+ if (Number.isFinite(lat) && Number.isFinite(lng)) return { lat, lng };
3058
+ }
3059
+ return null;
3060
+ }
3061
+ function Map2({
3062
+ className = "",
3063
+ id = null,
3064
+ style = null,
3065
+ height = "600px",
3066
+ tileLayers = [],
3067
+ scrollWheelZoom = false,
3068
+ cluster = true,
3069
+ customPoints = [],
3070
+ navDataset = null,
3071
+ iiifContent = null,
3072
+ defaultCenter = null,
3073
+ defaultZoom = null
3074
+ } = {}) {
3075
+ const containerRef = React34.useRef(null);
3076
+ const mapRef = React34.useRef(null);
3077
+ const layerRef = React34.useRef(null);
3078
+ const [leafletLib, setLeafletLib] = React34.useState(() => resolveGlobalLeaflet());
3079
+ const [leafletError, setLeafletError] = React34.useState(null);
3080
+ const datasetInfo = navDataset && typeof navDataset === "object" ? navDataset : null;
3081
+ const datasetHref = datasetInfo && datasetInfo.href || "/api/navplace.json";
3082
+ const datasetVersion = datasetInfo && datasetInfo.version;
3083
+ const datasetHasFeatures = !!(datasetInfo && datasetInfo.hasFeatures);
3084
+ const [navState, setNavState] = React34.useState(() => ({
3085
+ loading: false,
3086
+ error: null,
3087
+ markers: []
3088
+ }));
3089
+ const [iiifTargets, setIiifTargets] = React34.useState(() => ({
3090
+ loading: false,
3091
+ error: null,
3092
+ keys: []
3093
+ }));
3094
+ React34.useEffect(() => {
3095
+ if (!iiifContent) {
3096
+ setIiifTargets({ loading: false, error: null, keys: [] });
3097
+ return;
3098
+ }
3099
+ if (typeof iiifContent === "object") {
3100
+ const keys = extractManifestKeysFromIiif(iiifContent, "");
3101
+ setIiifTargets({
3102
+ loading: false,
3103
+ error: keys.length ? null : "No IIIF manifests were found for this resource.",
3104
+ keys
3105
+ });
3106
+ return;
3107
+ }
3108
+ const target = String(iiifContent || "").trim();
3109
+ if (!target) {
3110
+ setIiifTargets({ loading: false, error: null, keys: [] });
3111
+ return;
3112
+ }
3113
+ let cancelled = false;
3114
+ setIiifTargets({ loading: true, error: null, keys: [] });
3115
+ const iiifUrl = withBasePath(target);
3116
+ fetch(iiifUrl).then((res) => {
3117
+ if (!res.ok) throw new Error(`Failed to load IIIF content (${res.status})`);
3118
+ return res.json();
3119
+ }).then((json) => {
3120
+ if (cancelled) return;
3121
+ const keys = extractManifestKeysFromIiif(json, target);
3122
+ setIiifTargets({
3123
+ loading: false,
3124
+ error: keys.length ? null : "No IIIF manifests were found for this resource.",
3125
+ keys
3126
+ });
3127
+ }).catch((error) => {
3128
+ if (cancelled) return;
3129
+ setIiifTargets({
3130
+ loading: false,
3131
+ error: error && error.message ? error.message : "Failed to load IIIF content",
3132
+ keys: []
3133
+ });
3134
+ });
3135
+ return () => {
3136
+ cancelled = true;
3137
+ };
3138
+ }, [iiifContent]);
3139
+ const navTargets = iiifTargets.keys || [];
3140
+ const navTargetsKey = navTargets.join("|");
3141
+ const shouldFetchNav = datasetHasFeatures && navTargets.length > 0;
3142
+ React34.useEffect(() => {
3143
+ if (!shouldFetchNav) {
3144
+ setNavState({ loading: false, error: null, markers: [] });
3145
+ return void 0;
3146
+ }
3147
+ let cancelled = false;
3148
+ setNavState({ loading: true, error: null, markers: [] });
3149
+ const url = (() => {
3150
+ const base = withBasePath(datasetHref);
3151
+ if (!datasetVersion) return base;
3152
+ const joiner = base.includes("?") ? "&" : "?";
3153
+ return `${base}${joiner}v=${encodeURIComponent(datasetVersion)}`;
3154
+ })();
3155
+ fetch(url).then((res) => {
3156
+ if (!res.ok) throw new Error(`Failed to load map data (${res.status})`);
3157
+ return res.json();
3158
+ }).then((json) => {
3159
+ if (cancelled) return;
3160
+ const markers = extractNavMarkers(json, new Set(navTargets));
3161
+ setNavState({ loading: false, error: null, markers });
3162
+ }).catch((error) => {
3163
+ if (cancelled) return;
3164
+ setNavState({
3165
+ loading: false,
3166
+ error: error && error.message ? error.message : "Failed to load map data",
3167
+ markers: []
3168
+ });
3169
+ });
3170
+ return () => {
3171
+ cancelled = true;
3172
+ };
3173
+ }, [datasetHref, datasetVersion, navTargetsKey, shouldFetchNav]);
3174
+ React34.useEffect(() => {
3175
+ if (leafletLib) return;
3176
+ let cancelled = false;
3177
+ waitForLeaflet().then((lib) => {
3178
+ if (!cancelled) setLeafletLib(lib);
3179
+ }).catch((error) => {
3180
+ if (!cancelled) setLeafletError(error);
3181
+ });
3182
+ return () => {
3183
+ cancelled = true;
3184
+ };
3185
+ }, [leafletLib]);
3186
+ const navMatchMap = React34.useMemo(() => {
3187
+ const matchMap = createMarkerMap();
3188
+ (navState.markers || []).forEach((marker) => {
3189
+ if (!marker || !Array.isArray(marker.matchKeys)) return;
3190
+ marker.matchKeys.forEach((key) => {
3191
+ if (!key || matchMap.has(key)) return;
3192
+ matchMap.set(key, marker);
3193
+ });
3194
+ });
3195
+ return matchMap;
3196
+ }, [navState.markers]);
3197
+ const normalizedCustom = React34.useMemo(() => {
3198
+ return normalizeCustomMarkers(customPoints).map((point) => {
3199
+ if (!point || point.thumbnail || !point.href) return point;
3200
+ const match = navMatchMap.get(normalizeKey(point.href));
3201
+ if (!match || !match.thumbnail) return point;
3202
+ return {
3203
+ ...point,
3204
+ thumbnail: match.thumbnail,
3205
+ thumbnailWidth: match.thumbnailWidth || point.thumbnailWidth,
3206
+ thumbnailHeight: match.thumbnailHeight || point.thumbnailHeight,
3207
+ manifestTitle: match.manifestTitle || point.manifestTitle,
3208
+ manifestSummary: match.manifestSummary || point.manifestSummary
3209
+ };
3210
+ });
3211
+ }, [customPoints, navMatchMap]);
3212
+ const allMarkers = React34.useMemo(() => {
3213
+ return [...navState.markers || [], ...normalizedCustom];
3214
+ }, [navState.markers, normalizedCustom]);
3215
+ const clusterOptions = React34.useMemo(() => buildClusterOptions(leafletLib), [leafletLib]);
3216
+ React34.useEffect(() => {
3217
+ if (!containerRef.current || mapRef.current || !leafletLib) return void 0;
3218
+ const map = leafletLib.map(containerRef.current, {
3219
+ zoomControl: true,
3220
+ scrollWheelZoom: scrollWheelZoom === true
3221
+ });
3222
+ mapRef.current = map;
3223
+ const layers = buildTileLayers(tileLayers, leafletLib);
3224
+ const layerControlEntries = {};
3225
+ layers.forEach((entry, index) => {
3226
+ try {
3227
+ if (index === 0) entry.layer.addTo(map);
3228
+ layerControlEntries[entry.name || `Layer ${index + 1}`] = entry.layer;
3229
+ } catch (_) {
3230
+ }
3231
+ });
3232
+ if (Object.keys(layerControlEntries).length > 1) {
3233
+ leafletLib.control.layers(layerControlEntries, {}).addTo(map);
3234
+ }
3235
+ const supportsClusters = typeof leafletLib.markerClusterGroup === "function";
3236
+ const layerGroup = cluster !== false && supportsClusters ? leafletLib.markerClusterGroup(clusterOptions || { chunkedLoading: true }) : leafletLib.layerGroup();
3237
+ layerGroup.addTo(map);
3238
+ layerRef.current = layerGroup;
3239
+ setTimeout(() => {
3240
+ try {
3241
+ map.invalidateSize();
3242
+ } catch (_) {
3243
+ }
3244
+ }, 0);
3245
+ return () => {
3246
+ try {
3247
+ map.remove();
3248
+ } catch (_) {
3249
+ }
3250
+ mapRef.current = null;
3251
+ layerRef.current = null;
3252
+ };
3253
+ }, [tileLayers, scrollWheelZoom, cluster, clusterOptions, leafletLib]);
3254
+ React34.useEffect(() => {
3255
+ const map = mapRef.current;
3256
+ const layer = layerRef.current;
3257
+ if (!map || !layer || !leafletLib) return;
3258
+ try {
3259
+ layer.clearLayers();
3260
+ } catch (_) {
3261
+ }
3262
+ const bounds = [];
3263
+ allMarkers.forEach((marker) => {
3264
+ if (!marker || !Number.isFinite(marker.lat) || !Number.isFinite(marker.lng)) return;
3265
+ const latlng = leafletLib.latLng(marker.lat, marker.lng);
3266
+ bounds.push(latlng);
3267
+ const icon = buildMarkerIcon(marker, leafletLib);
3268
+ const leafletMarker = leafletLib.marker(latlng, icon ? { icon } : void 0);
3269
+ const popup = renderPopup(marker);
3270
+ if (popup) {
3271
+ try {
3272
+ leafletMarker.bindPopup(popup);
3273
+ } catch (_) {
3274
+ }
3275
+ }
3276
+ try {
3277
+ layer.addLayer(leafletMarker);
3278
+ } catch (_) {
3279
+ }
3280
+ });
3281
+ const centerOverride = normalizeCenterInput(defaultCenter);
3282
+ const hasDefaultZoom = Number.isFinite(defaultZoom);
3283
+ if (hasDefaultZoom) {
3284
+ let targetCenter = centerOverride;
3285
+ if (!targetCenter && bounds.length) {
3286
+ try {
3287
+ const mapBounds = leafletLib.latLngBounds(bounds);
3288
+ const center = mapBounds.getCenter();
3289
+ targetCenter = { lat: center.lat, lng: center.lng };
3290
+ } catch (_) {
3291
+ }
3292
+ }
3293
+ try {
3294
+ if (targetCenter) {
3295
+ map.setView([targetCenter.lat, targetCenter.lng], defaultZoom);
3296
+ } else {
3297
+ map.setZoom(defaultZoom);
3298
+ }
3299
+ } catch (_) {
3300
+ }
3301
+ return;
3302
+ }
3303
+ if (bounds.length) {
3304
+ try {
3305
+ const mapBounds = leafletLib.latLngBounds(bounds);
3306
+ map.fitBounds(mapBounds, { padding: [32, 32] });
3307
+ } catch (_) {
3308
+ }
3309
+ return;
3310
+ }
3311
+ if (centerOverride) {
3312
+ try {
3313
+ map.setView([centerOverride.lat, centerOverride.lng], 2);
3314
+ } catch (_) {
3315
+ }
3316
+ }
3317
+ }, [allMarkers, defaultCenter, defaultZoom, leafletLib]);
3318
+ const isLoadingMarkers = iiifTargets.loading || navState.loading;
3319
+ const hasMarkers = allMarkers.length > 0;
3320
+ const hasCustomPoints = normalizedCustom.length > 0;
3321
+ const datasetUnavailable = navTargets.length > 0 && !datasetHasFeatures;
3322
+ const rootClass = [
3323
+ "canopy-map",
3324
+ className,
3325
+ isLoadingMarkers ? "canopy-map--loading" : null,
3326
+ iiifTargets.error || navState.error || datasetUnavailable ? "canopy-map--error" : null
3327
+ ].filter(Boolean).join(" ");
3328
+ const statusLabel = leafletError ? leafletError.message || "Failed to load map library" : !leafletLib ? "Loading map\u2026" : iiifTargets.error ? iiifTargets.error : datasetUnavailable ? "Map data is unavailable for this site." : navState.error ? navState.error : isLoadingMarkers ? "Loading map data\u2026" : !iiifContent && !hasCustomPoints ? "Add iiifContent or MapPoint markers to populate this map." : !hasMarkers ? "No map locations available." : "";
3329
+ const showStatus = Boolean(statusLabel);
3330
+ return /* @__PURE__ */ React34.createElement("div", { className: rootClass, id: id || void 0, style: style || void 0 }, /* @__PURE__ */ React34.createElement(
3331
+ "div",
3332
+ {
3333
+ ref: containerRef,
3334
+ className: "canopy-map__canvas",
3335
+ style: { height: height || "600px" }
3336
+ }
3337
+ ), showStatus ? /* @__PURE__ */ React34.createElement("div", { className: "canopy-map__status", "aria-live": "polite" }, statusLabel) : null);
3338
+ }
3339
+
3340
+ // ui/src/content/map/MapPoint.jsx
3341
+ function MapPoint() {
3342
+ return null;
3343
+ }
3344
+ MapPoint.displayName = "MapPoint";
2695
3345
  export {
2696
3346
  ArticleCard,
2697
3347
  Button,
@@ -2709,6 +3359,8 @@ export {
2709
3359
  GridItem,
2710
3360
  HelloWorld,
2711
3361
  Image,
3362
+ Map2 as Map,
3363
+ MapPoint,
2712
3364
  MdxRelatedItems as RelatedItems,
2713
3365
  Scroll,
2714
3366
  MdxSearch as Search,