@canopy-iiif/app 1.3.6 → 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.
@@ -0,0 +1,227 @@
1
+ const crypto = require('crypto');
2
+ const {
3
+ fs,
4
+ fsp,
5
+ path,
6
+ OUT_DIR,
7
+ ensureDirSync,
8
+ rootRelativeHref,
9
+ } = require('../common');
10
+ const {firstLabelString} = require('./featured');
11
+
12
+ const NAVPLACE_RELATIVE = path.join('api', 'navplace.json');
13
+ const NAVPLACE_PATH = path.join(OUT_DIR, NAVPLACE_RELATIVE);
14
+ const NAVPLACE_PUBLIC_HREF = rootRelativeHref(
15
+ NAVPLACE_RELATIVE.split(path.sep).join('/')
16
+ );
17
+
18
+ let cachedSummary = null;
19
+
20
+ function normalizeStringValue(value) {
21
+ if (value == null) return '';
22
+ if (typeof value === 'string') return value.trim();
23
+ if (typeof value === 'number' || typeof value === 'boolean') {
24
+ return String(value);
25
+ }
26
+ if (Array.isArray(value)) {
27
+ for (const entry of value) {
28
+ const text = normalizeStringValue(entry);
29
+ if (text) return text;
30
+ }
31
+ return '';
32
+ }
33
+ if (typeof value === 'object') {
34
+ const label = firstLabelString(value);
35
+ if (label && label !== 'Untitled') return label;
36
+ const keys = Object.keys(value);
37
+ for (const key of keys) {
38
+ const text = normalizeStringValue(value[key]);
39
+ if (text) return text;
40
+ }
41
+ }
42
+ return '';
43
+ }
44
+
45
+ function normalizeCoordinate(value) {
46
+ const num = Number(value);
47
+ return Number.isFinite(num) ? num : null;
48
+ }
49
+
50
+ function sanitizeFeature(feature, index, context) {
51
+ if (!feature || typeof feature !== 'object') return null;
52
+ const geometry = feature.geometry || {};
53
+ const geomType = typeof geometry.type === 'string' ? geometry.type : '';
54
+ if (geomType.toLowerCase() !== 'point') return null;
55
+ const coords = Array.isArray(geometry.coordinates)
56
+ ? geometry.coordinates
57
+ : [];
58
+ const lng = normalizeCoordinate(coords[0]);
59
+ const lat = normalizeCoordinate(coords[1]);
60
+ if (lat == null || lng == null) return null;
61
+ const properties = feature.properties || {};
62
+ const label = normalizeStringValue(properties.label) || context.title;
63
+ const summary = normalizeStringValue(
64
+ properties.summary || properties.description || properties.note
65
+ );
66
+ const idBase = context.slug || context.id || 'manifest';
67
+ const normalizedId = feature.id
68
+ ? String(feature.id)
69
+ : `${idBase}-point-${index + 1}`;
70
+ return {
71
+ id: normalizedId,
72
+ label: label || context.title,
73
+ summary: summary || context.summary || '',
74
+ lat,
75
+ lng,
76
+ };
77
+ }
78
+
79
+ function collectManifestFeatures(manifest) {
80
+ if (!manifest) return [];
81
+ const navPlace = manifest.navPlace || manifest.navplace;
82
+ if (!navPlace) return [];
83
+ if (Array.isArray(navPlace)) return navPlace;
84
+ if (Array.isArray(navPlace.features)) return navPlace.features;
85
+ if (navPlace.type && navPlace.type.toLowerCase() === 'feature') {
86
+ return [navPlace];
87
+ }
88
+ return [];
89
+ }
90
+
91
+ function buildManifestNavPlaceRecord(options = {}) {
92
+ const manifest = options.manifest || null;
93
+ if (!manifest) return null;
94
+ const rawFeatures = collectManifestFeatures(manifest);
95
+ if (!rawFeatures.length) return null;
96
+ const manifestId = manifest.id || manifest['@id'] || '';
97
+ const title = normalizeStringValue(options.title) || firstLabelString(manifest.label);
98
+ const summary = normalizeStringValue(options.summary) || normalizeStringValue(manifest.summary);
99
+ const slug = options.slug ? String(options.slug) : '';
100
+ const href = options.href ? String(options.href) : '';
101
+ const thumbnail = options.thumbnail ? String(options.thumbnail) : '';
102
+ const thumbWidth =
103
+ typeof options.thumbnailWidth === 'number' ? options.thumbnailWidth : undefined;
104
+ const thumbHeight =
105
+ typeof options.thumbnailHeight === 'number' ? options.thumbnailHeight : undefined;
106
+ const context = {
107
+ id: manifestId,
108
+ slug,
109
+ title: title || 'Untitled',
110
+ summary: summary || '',
111
+ };
112
+ const features = rawFeatures
113
+ .map((feature, index) => sanitizeFeature(feature, index, context))
114
+ .filter(Boolean);
115
+ if (!features.length) return null;
116
+ return {
117
+ id: String(manifestId || slug || context.title || ''),
118
+ slug,
119
+ href,
120
+ title: context.title,
121
+ summary: context.summary,
122
+ thumbnail,
123
+ thumbnailWidth: thumbWidth,
124
+ thumbnailHeight: thumbHeight,
125
+ features,
126
+ };
127
+ }
128
+
129
+ function stableCopy(records) {
130
+ return records
131
+ .map((record) => ({
132
+ ...record,
133
+ features: (record.features || [])
134
+ .slice()
135
+ .sort((a, b) => (a.id || '').localeCompare(b.id || ''))
136
+ .map((feat) => ({
137
+ id: String(feat.id || ''),
138
+ label: feat.label || '',
139
+ summary: feat.summary || '',
140
+ lat: Number(feat.lat),
141
+ lng: Number(feat.lng),
142
+ })),
143
+ }))
144
+ .sort((a, b) => (a.href || '').localeCompare(b.href || ''));
145
+ }
146
+
147
+ function hashRecords(records) {
148
+ try {
149
+ const stable = stableCopy(records);
150
+ const payload = JSON.stringify(stable);
151
+ return crypto.createHash('sha1').update(payload).digest('hex');
152
+ } catch (_) {
153
+ return '';
154
+ }
155
+ }
156
+
157
+ async function removeDataset() {
158
+ cachedSummary = {
159
+ hasFeatures: false,
160
+ version: null,
161
+ href: NAVPLACE_PUBLIC_HREF,
162
+ };
163
+ try {
164
+ await fsp.rm(NAVPLACE_PATH, {force: true});
165
+ } catch (_) {}
166
+ return cachedSummary;
167
+ }
168
+
169
+ async function writeNavPlaceDataset(records = []) {
170
+ const valid = Array.isArray(records)
171
+ ? records.filter((record) => record && record.features && record.features.length)
172
+ : [];
173
+ if (!valid.length) {
174
+ return removeDataset();
175
+ }
176
+ const stable = stableCopy(valid);
177
+ const version = hashRecords(valid);
178
+ const payload = {
179
+ version,
180
+ generatedAt: new Date().toISOString(),
181
+ manifests: stable,
182
+ };
183
+ ensureDirSync(path.dirname(NAVPLACE_PATH));
184
+ await fsp.writeFile(NAVPLACE_PATH, JSON.stringify(payload, null, 2), 'utf8');
185
+ cachedSummary = {
186
+ hasFeatures: true,
187
+ version: version || null,
188
+ href: NAVPLACE_PUBLIC_HREF,
189
+ };
190
+ return cachedSummary;
191
+ }
192
+
193
+ function readSummaryFromDisk() {
194
+ try {
195
+ const raw = fs.readFileSync(NAVPLACE_PATH, 'utf8');
196
+ const data = JSON.parse(raw);
197
+ const hasFeatures = Array.isArray(data.manifests)
198
+ ? data.manifests.some((entry) => entry && entry.features && entry.features.length)
199
+ : false;
200
+ return {
201
+ hasFeatures,
202
+ version: data.version || null,
203
+ href: NAVPLACE_PUBLIC_HREF,
204
+ };
205
+ } catch (_) {
206
+ return {
207
+ hasFeatures: false,
208
+ version: null,
209
+ href: NAVPLACE_PUBLIC_HREF,
210
+ };
211
+ }
212
+ }
213
+
214
+ function getNavPlaceDatasetInfo() {
215
+ if (!cachedSummary) {
216
+ cachedSummary = readSummaryFromDisk();
217
+ }
218
+ return cachedSummary;
219
+ }
220
+
221
+ module.exports = {
222
+ buildManifestNavPlaceRecord,
223
+ writeNavPlaceDataset,
224
+ getNavPlaceDatasetInfo,
225
+ NAVPLACE_PATH,
226
+ NAVPLACE_PUBLIC_HREF,
227
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@canopy-iiif/app",
3
- "version": "1.3.6",
3
+ "version": "1.4.0",
4
4
  "private": false,
5
5
  "license": "MIT",
6
6
  "author": "Mat Jordan <mat@northwestern.edu>",
@@ -57,14 +57,16 @@
57
57
  "access": "public"
58
58
  },
59
59
  "dependencies": {
60
- "@radix-ui/colors": "^3.0.0",
61
60
  "@iiif/helpers": "^1.4.0",
62
61
  "@mdx-js/mdx": "^3.1.0",
63
62
  "@mdx-js/react": "^3.1.0",
63
+ "@radix-ui/colors": "^3.0.0",
64
64
  "charm": "^1.0.2",
65
65
  "js-yaml": "^4.1.0",
66
- "remark-gfm": "^4.0.0",
66
+ "leaflet": "^1.9.4",
67
+ "leaflet.markercluster": "^1.5.3",
67
68
  "react-masonry-css": "^1.0.16",
69
+ "remark-gfm": "^4.0.0",
68
70
  "sass": "^1.77.0",
69
71
  "slugify": "^1.6.6"
70
72
  },