@projektemacher/storymapjs 0.10.8 → 0.11.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/CHANGELOG +81 -0
- package/README.md +129 -12
- package/dist/assets/{demo-DBpeF31K.js → demo-bWnOyfUw.js} +1 -1
- package/dist/assets/harness-C3JUv5Ii.js +1 -0
- package/dist/assets/{harness-multi-CYHgI0a9.js → harness-multi-XZNex3tb.js} +1 -1
- package/dist/assets/{harness-support-Do7sIp2j.js → harness-support-CjfZIVeb.js} +1 -1
- package/dist/assets/main-DQ32hWBM.js +17 -0
- package/dist/context.json +13 -2
- package/dist/css/storymap.css +1 -1
- package/dist/demo.html +3 -2
- package/dist/docs/api/assets/navigation.js +1 -1
- package/dist/docs/api/assets/search.js +1 -1
- package/dist/docs/api/classes/StoryMap.html +777 -269
- package/dist/docs/api/functions/MediaType.html +1 -1
- package/dist/docs/api/functions/isPresentation3Collection.html +1 -1
- package/dist/docs/api/functions/isPresentation3Manifest.html +1 -1
- package/dist/docs/api/functions/isSlideshowCollection.html +51 -0
- package/dist/docs/api/functions/loadCSS.html +1 -1
- package/dist/docs/api/functions/manifestToStorymapData.html +1 -1
- package/dist/docs/api/functions/setLanguage.html +1 -1
- package/dist/docs/api/functions/slideshowToStorymapData.html +51 -0
- package/dist/docs/api/functions/slideshowVersion.html +48 -0
- package/dist/docs/api/functions/storymapToManifest.html +1 -1
- package/dist/docs/api/functions/validateStorymap.html +1 -1
- package/dist/docs/api/functions/validateStorymapAndReport.html +1 -1
- package/dist/docs/api/index.html +115 -12
- package/dist/docs/api/interfaces/AnimationHandle.html +2 -2
- package/dist/docs/api/interfaces/ImagereadyPayload.html +4 -4
- package/dist/docs/api/interfaces/LanguageEntry.html +6 -6
- package/dist/docs/api/interfaces/LatLngLiteral.html +4 -4
- package/dist/docs/api/interfaces/LoadOptions.html +2 -2
- package/dist/docs/api/interfaces/MapEvents.html +116 -0
- package/dist/docs/api/interfaces/MapMarkerEvents.html +66 -0
- package/dist/docs/api/interfaces/MarkerEventPayload.html +57 -0
- package/dist/docs/api/interfaces/MediaEvents.html +78 -0
- package/dist/docs/api/interfaces/MenuBarEvents.html +114 -0
- package/dist/docs/api/interfaces/MessageEvents.html +72 -0
- package/dist/docs/api/interfaces/OlLayer.html +1 -1
- package/dist/docs/api/interfaces/OlMap.html +1 -1
- package/dist/docs/api/interfaces/OlSource.html +1 -1
- package/dist/docs/api/interfaces/OlTileLayer.html +1 -1
- package/dist/docs/api/interfaces/OlVectorLayer.html +1 -1
- package/dist/docs/api/interfaces/OlView.html +1 -1
- package/dist/docs/api/interfaces/SlideBackgroundState.html +66 -0
- package/dist/docs/api/interfaces/SlideEvents.html +78 -0
- package/dist/docs/api/interfaces/SlideNavEvents.html +72 -0
- package/dist/docs/api/interfaces/SlideshowPlayerSettings.html +182 -0
- package/dist/docs/api/interfaces/SlideshowToStorymapResult.html +60 -0
- package/dist/docs/api/interfaces/SlideshowTranslatorOptions.html +65 -0
- package/dist/docs/api/interfaces/SlideshowWarning.html +72 -0
- package/dist/docs/api/interfaces/StoryMapEvents.html +146 -0
- package/dist/docs/api/interfaces/StoryMapMarker.html +763 -0
- package/dist/docs/api/interfaces/StorySliderEvents.html +102 -0
- package/dist/docs/api/interfaces/StorymapBasemap.html +69 -0
- package/dist/docs/api/interfaces/StorymapData.html +12 -6
- package/dist/docs/api/interfaces/StorymapDataWrapper.html +2 -2
- package/dist/docs/api/interfaces/StorymapDocument.html +2 -2
- package/dist/docs/api/interfaces/StorymapError.html +3 -3
- package/dist/docs/api/interfaces/StorymapGeoreference.html +5 -5
- package/dist/docs/api/interfaces/StorymapGeoreferenceBody.html +4 -4
- package/dist/docs/api/interfaces/StorymapGroundControlPoint.html +4 -4
- package/dist/docs/api/interfaces/StorymapLanguageMap.html +2 -2
- package/dist/docs/api/interfaces/StorymapManifest.html +12 -12
- package/dist/docs/api/interfaces/StorymapManifestAnnotation.html +43 -11
- package/dist/docs/api/interfaces/StorymapManifestBackground.html +6 -6
- package/dist/docs/api/interfaces/StorymapManifestCanvas.html +65 -17
- package/dist/docs/api/interfaces/StorymapManifestContentResource.html +8 -8
- package/dist/docs/api/interfaces/StorymapManifestFeatureCollection.html +4 -4
- package/dist/docs/api/interfaces/StorymapManifestGeoreferencing.html +7 -7
- package/dist/docs/api/interfaces/StorymapManifestImageService.html +4 -4
- package/dist/docs/api/interfaces/StorymapManifestRange.html +5 -5
- package/dist/docs/api/interfaces/StorymapManifestService.html +4 -4
- package/dist/docs/api/interfaces/StorymapManifestSpecificResource.html +4 -4
- package/dist/docs/api/interfaces/StorymapManifestStatement.html +3 -3
- package/dist/docs/api/interfaces/StorymapMapOptions.html +5 -5
- package/dist/docs/api/interfaces/StorymapOptions.html +277 -75
- package/dist/docs/api/interfaces/StorymapOverlayLayer.html +42 -11
- package/dist/docs/api/interfaces/StorymapSlide.html +70 -16
- package/dist/docs/api/interfaces/StorymapSlideBackground.html +4 -4
- package/dist/docs/api/interfaces/StorymapSlideLocation.html +58 -14
- package/dist/docs/api/interfaces/StorymapSlideMarker.html +8 -9
- package/dist/docs/api/interfaces/StorymapSlideMedia.html +49 -10
- package/dist/docs/api/interfaces/StorymapSlideNarration.html +61 -9
- package/dist/docs/api/interfaces/StorymapSlideText.html +4 -4
- package/dist/docs/api/interfaces/StorymapTilejson.html +8 -8
- package/dist/docs/api/interfaces/SwipableEvents.html +102 -0
- package/dist/docs/api/interfaces/TextEvents.html +66 -0
- package/dist/docs/api/media/events.md +66 -0
- package/dist/docs/api/media/migration-from-knightlab.md +89 -49
- package/dist/docs/api/media/slideshow.md +109 -0
- package/dist/docs/api/media/storymap-as-iiif-manifest.md +1007 -0
- package/dist/docs/api/modules.html +6 -6
- package/dist/docs/api/types/ContentState.html +3 -3
- package/dist/docs/api/types/FiredEvent.html +43 -0
- package/dist/docs/api/types/ImagereadyKind.html +1 -1
- package/dist/docs/api/types/MediaTypeMatch.html +5 -5
- package/dist/docs/api/types/SwipeDirection.html +36 -0
- package/dist/docs/api/types/TileSourceFactory.html +1 -1
- package/dist/docs/authoring.html +20 -7
- package/dist/docs/events.html +263 -0
- package/dist/docs/iiif.html +136 -22
- package/dist/docs/migration.html +66 -19
- package/dist/docs/readme.html +113 -12
- package/dist/embed/index.html +7 -0
- package/dist/examples/font-css-false.json +33 -0
- package/dist/examples/issue-image-region.json +1 -1
- package/dist/examples/layers-control.json +60 -0
- package/dist/examples/map-template-provider.json +36 -0
- package/dist/examples/multi-media.json +71 -0
- package/dist/examples-iiif/annotated-image.json +16 -0
- package/dist/examples-iiif/font-css-false.json +114 -0
- package/dist/examples-iiif/issue-image-region.json +1 -1
- package/dist/examples-iiif/layers-control.json +226 -0
- package/dist/examples-iiif/map-template-provider.json +133 -0
- package/dist/examples-iiif/multi-media.json +237 -0
- package/dist/examples-slideshow/single.json +79 -0
- package/dist/examples-slideshow/static/pixel.png +0 -0
- package/dist/examples-slideshow/static.json +44 -0
- package/dist/harness-multi.html +3 -3
- package/dist/harness.html +4 -3
- package/dist/index.html +3 -2
- package/dist/js/storymap.d.ts +1326 -289
- package/dist/js/storymap.js +6690 -4784
- package/dist/js/storymap.js.map +1 -1
- package/dist/navplace-properties.json +14 -0
- package/dist/site.css +54 -36
- package/docs/migration-from-knightlab.md +89 -49
- package/docs/storymap-as-iiif-manifest.md +199 -126
- package/package.json +4 -2
- package/schema/storymap.schema.json +334 -34
- package/src/core/Util.ts +3 -1
- package/src/core/mixins.ts +42 -28
- package/src/language/Language.ts +7 -0
- package/src/language/locale/.expected-gaps.json +287 -27
- package/src/language/locale/de.json +11 -1
- package/src/language/locale/en.json +11 -1
- package/src/main.ts +51 -0
- package/src/map/Map.ts +44 -6
- package/src/map/MapMarker.ts +16 -3
- package/src/map/openlayers/Map.OpenLayers.ts +787 -62
- package/src/map/openlayers/MapMarker.OpenLayers.ts +113 -13
- package/src/map/slideView.ts +149 -0
- package/src/map/types.ts +2 -0
- package/src/media/EmbedUtil.ts +27 -2
- package/src/media/Media.ts +27 -4
- package/src/media/embedId.ts +6 -0
- package/src/media/types/Flickr.ts +14 -3
- package/src/media/types/HtmlMedia.ts +66 -0
- package/src/media/types/Image.ts +15 -3
- package/src/media/types/SoundCloud.ts +1 -1
- package/src/media/types/Text.ts +27 -8
- package/src/media/types/Twitter.ts +9 -7
- package/src/media/types/Vimeo.ts +1 -1
- package/src/media/types/YouTube.ts +2 -2
- package/src/scss/Typography.scss +15 -15
- package/src/scss/VCO.StoryMap.scss +173 -8
- package/src/scss/core/Reset.scss +5 -18
- package/src/scss/map/VCO.Map.scss +1 -1
- package/src/scss/map/VCO.MapMarker.scss +20 -9
- package/src/scss/map/openlayers/VCO.Map.OpenLayers.scss +7 -4
- package/src/scss/media/VCO.Media.scss +7 -7
- package/src/scss/media/types/VCO.Media.Blockquote.scss +1 -1
- package/src/scss/media/types/VCO.Media.Text.scss +5 -5
- package/src/scss/media/types/VCO.Media.Twitter.scss +4 -4
- package/src/scss/media/types/VCO.Media.Wikipedia.scss +2 -19
- package/src/scss/site/site.scss +55 -36
- package/src/scss/slider/VCO.Slide.scss +49 -11
- package/src/scss/slider/VCO.SlideNav.scss +23 -7
- package/src/scss/slider/VCO.StorySlider.scss +50 -0
- package/src/scss/snapmap/VCO.SnapMap.scss +3 -3
- package/src/scss/ui/VCO.Consent.scss +20 -14
- package/src/scss/ui/VCO.Layers.scss +119 -0
- package/src/scss/ui/VCO.MenuBar.Button.scss +140 -19
- package/src/scss/ui/VCO.MenuBar.scss +9 -2
- package/src/scss/ui/VCO.Message.scss +14 -0
- package/src/slider/Slide.ts +232 -74
- package/src/slider/SlideNav.ts +15 -4
- package/src/slider/StorySlider.ts +203 -22
- package/src/storymap/Consent.ts +22 -0
- package/src/storymap/StoryMap.ts +944 -131
- package/src/storymap/content-state.ts +3 -8
- package/src/storymap/font-css.ts +72 -0
- package/src/storymap/from-slideshow.ts +785 -0
- package/src/storymap/iiif-shared.ts +228 -0
- package/src/storymap/iiif.ts +289 -100
- package/src/storymap/to-iiif.ts +131 -44
- package/src/types.ts +382 -17
- package/src/ui/LayersControl.ts +302 -0
- package/src/ui/MenuBar.ts +232 -21
- package/src/ui/Message.ts +36 -3
- package/src/ui/Swipable.ts +25 -7
- package/dist/assets/harness-Cr0rYpvt.js +0 -1
- package/dist/assets/main-DXalctuR.js +0 -17
- package/src/scss/VCO.StoryMap.Dark.scss +0 -150
- package/src/scss/Variables.Dark.scss +0 -57
package/src/storymap/iiif.ts
CHANGED
|
@@ -15,54 +15,24 @@ import type {
|
|
|
15
15
|
StorymapSlideMedia,
|
|
16
16
|
} from "../types";
|
|
17
17
|
import { readGroundControlPoints } from "../map/georeference";
|
|
18
|
+
import {
|
|
19
|
+
LOCATION_PROPERTIES,
|
|
20
|
+
asBoolean,
|
|
21
|
+
asNumber,
|
|
22
|
+
asRecord,
|
|
23
|
+
asString,
|
|
24
|
+
asStringArray,
|
|
25
|
+
clampRegion,
|
|
26
|
+
escapeText,
|
|
27
|
+
isDuplicateNarration,
|
|
28
|
+
isLonLatBox,
|
|
29
|
+
readPlaybackBag,
|
|
30
|
+
} from "./iiif-shared";
|
|
18
31
|
|
|
19
32
|
const PRESENTATION_3_CONTEXT = "iiif.io/api/presentation/3/context.json";
|
|
20
33
|
const MAPCONFIG_PROFILE = "mapconfig";
|
|
21
34
|
const STORYMAP_PREFIX = "storymap:";
|
|
22
35
|
|
|
23
|
-
/** Keys copied verbatim from navPlace Feature properties onto the location. */
|
|
24
|
-
const LOCATION_PROPERTIES = [
|
|
25
|
-
"name",
|
|
26
|
-
"zoom",
|
|
27
|
-
"line",
|
|
28
|
-
"icon",
|
|
29
|
-
"iconSize",
|
|
30
|
-
"image",
|
|
31
|
-
"use_custom_marker",
|
|
32
|
-
// marker presentation with no IIIF vocabulary of its own: a GeoJSON
|
|
33
|
-
// foreign member needs no registration, which is why these live here
|
|
34
|
-
// rather than in a storymap: term (docs/plans/iiif-media-tours.md §2)
|
|
35
|
-
"popup",
|
|
36
|
-
"audioBadge",
|
|
37
|
-
] as const;
|
|
38
|
-
|
|
39
|
-
function asRecord(value: unknown): Record<string, unknown> | null {
|
|
40
|
-
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
41
|
-
return null;
|
|
42
|
-
}
|
|
43
|
-
return value as Record<string, unknown>;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function asString(value: unknown): string | null {
|
|
47
|
-
return typeof value === "string" && value !== "" ? value : null;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
function asNumber(value: unknown): number | null {
|
|
51
|
-
return typeof value === "number" && !isNaN(value) ? value : null;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
function asBoolean(value: unknown): boolean | null {
|
|
55
|
-
return typeof value === "boolean" ? value : null;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
function asStringArray(value: unknown): string[] {
|
|
59
|
-
if (typeof value === "string") return [value];
|
|
60
|
-
if (Array.isArray(value)) {
|
|
61
|
-
return value.filter((entry): entry is string => typeof entry === "string");
|
|
62
|
-
}
|
|
63
|
-
return [];
|
|
64
|
-
}
|
|
65
|
-
|
|
66
36
|
/**
|
|
67
37
|
* Reads a IIIF xywh image region (`storymap:imageRegion`): an array of
|
|
68
38
|
* exactly 4 finite numbers ([x, y, w, h] in image pixels). Returns null
|
|
@@ -161,8 +131,8 @@ function isEmptySelector(sel: ReadSelector): boolean {
|
|
|
161
131
|
|
|
162
132
|
/** A `PointSelector` is a pin, which the viewer cannot fit, so we synthesize
|
|
163
133
|
* a square of 5% of the canvas's smaller side, centred on the point and
|
|
164
|
-
* clamped to the canvas. Stated once, here
|
|
165
|
-
*
|
|
134
|
+
* clamped to the canvas. Stated once, here, so annotation-driven stops
|
|
135
|
+
* share it rather than re-deriving it. */
|
|
166
136
|
const POINT_SQUARE_FRACTION = 0.05;
|
|
167
137
|
|
|
168
138
|
function pointToRegion(
|
|
@@ -190,8 +160,11 @@ function readSelectorEntry(entry: unknown, out: ReadSelector, depth = 0): void {
|
|
|
190
160
|
const match = /xywh=(pixel:)?([^,]+),([^,]+),([^,]+),([^,]+)/.exec(raw);
|
|
191
161
|
if (match) {
|
|
192
162
|
const parts = match.slice(2).map(Number);
|
|
163
|
+
// Origins may be negative (slideshow overshoots its canvas);
|
|
164
|
+
// clamping to the canvas happens in readSelector, which knows
|
|
165
|
+
// the size. Only the size must already be positive here.
|
|
193
166
|
if (parts.length === 4 && parts.every((n) => Number.isFinite(n))) {
|
|
194
|
-
if (parts[
|
|
167
|
+
if (parts[2] > 0 && parts[3] > 0) {
|
|
195
168
|
out.region = parts as [number, number, number, number];
|
|
196
169
|
}
|
|
197
170
|
}
|
|
@@ -273,7 +246,14 @@ export function readSelector(
|
|
|
273
246
|
}
|
|
274
247
|
}
|
|
275
248
|
|
|
276
|
-
if (out.region
|
|
249
|
+
if (out.region !== null) {
|
|
250
|
+
const clamped = clampRegion(out.region, width, height);
|
|
251
|
+
out.region = clamped;
|
|
252
|
+
if (clamped === null && out.point !== null) {
|
|
253
|
+
// fully outside the canvas: fall back to the point square
|
|
254
|
+
out.region = pointToRegion(out.point, width, height);
|
|
255
|
+
}
|
|
256
|
+
} else if (out.point !== null) {
|
|
277
257
|
out.region = pointToRegion(out.point, width, height);
|
|
278
258
|
}
|
|
279
259
|
|
|
@@ -312,7 +292,7 @@ export function flattenLanguageMap(value: unknown): string {
|
|
|
312
292
|
* type 'object'" — so the array branch below is producer leniency, not
|
|
313
293
|
* conformance. The `label` ("Credit", "Rights holder", a language-tagged term)
|
|
314
294
|
* is half of what a real institutional manifest ships, and reading only
|
|
315
|
-
* `.value` dropped it.
|
|
295
|
+
* `.value` dropped it.
|
|
316
296
|
*/
|
|
317
297
|
function readRequiredStatement(statement: unknown): { label: string; value: string } {
|
|
318
298
|
const entries = Array.isArray(statement)
|
|
@@ -437,8 +417,7 @@ function collectionToStorymapData(collection: Record<string, unknown>): Storymap
|
|
|
437
417
|
"A Presentation 3 Collection references its members from other documents, and " +
|
|
438
418
|
"the converter is synchronous, so it contributes no slides rather than " +
|
|
439
419
|
"mangling them. To build a multi-manifest tour, fetch the members and " +
|
|
440
|
-
"concatenate their manifestToStorymapData() slides yourself
|
|
441
|
-
"(see docs/plans/iiif-interop.md §2.2).",
|
|
420
|
+
"concatenate their manifestToStorymapData() slides yourself.",
|
|
442
421
|
);
|
|
443
422
|
|
|
444
423
|
return data;
|
|
@@ -550,8 +529,8 @@ function readMapConfig(manifest: Record<string, unknown>): Record<string, unknow
|
|
|
550
529
|
/**
|
|
551
530
|
* What a painting annotation tells us about a slide's media.
|
|
552
531
|
*
|
|
553
|
-
* This is the **shared body record**
|
|
554
|
-
*
|
|
532
|
+
* This is the **shared body record** for annotation-driven stops:
|
|
533
|
+
* `type` is how a `Sound` body is told
|
|
555
534
|
* from an `Image` or a `TextualBody`. Anything else that needs a field from
|
|
556
535
|
* the body belongs on this record rather than in a second reader.
|
|
557
536
|
*/
|
|
@@ -582,6 +561,27 @@ export interface PaintingBody {
|
|
|
582
561
|
* in storymap JSON is the documented route.
|
|
583
562
|
*/
|
|
584
563
|
subtitles: string | null;
|
|
564
|
+
/**
|
|
565
|
+
* Playback flags from `storymap:` foreign members on the painting
|
|
566
|
+
* annotation (slideshow audio terms); absent stays absent.
|
|
567
|
+
*/
|
|
568
|
+
offset?: number;
|
|
569
|
+
loop?: boolean;
|
|
570
|
+
play?: "auto" | "click";
|
|
571
|
+
stopOnExit?: boolean;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
/**
|
|
575
|
+
* Reads `storymap:` playback flags off a painting annotation into the
|
|
576
|
+
* painting body: `offset` (seconds), `loop`, `play` and `stopOnExit`
|
|
577
|
+
* (painting annotations never carry `stopAllPrevious`).
|
|
578
|
+
*/
|
|
579
|
+
function readPaintingPlayback(
|
|
580
|
+
annotationRecord: Record<string, unknown>,
|
|
581
|
+
painting: { offset?: number; loop?: boolean; play?: "auto" | "click"; stopOnExit?: boolean },
|
|
582
|
+
): void {
|
|
583
|
+
const { stopAllPrevious: _dropped, ...bag } = readPlaybackBag(annotationRecord);
|
|
584
|
+
Object.assign(painting, bag);
|
|
585
585
|
}
|
|
586
586
|
|
|
587
587
|
/**
|
|
@@ -642,18 +642,101 @@ function readFirstTypedBody(body: unknown): Record<string, unknown> | null {
|
|
|
642
642
|
return url === null ? null : record;
|
|
643
643
|
}
|
|
644
644
|
|
|
645
|
+
/** Every body of a `body` array that yields a URL, in order (#358). */
|
|
646
|
+
function readPaintingBodies(body: unknown): Record<string, unknown>[] {
|
|
647
|
+
if (Array.isArray(body)) {
|
|
648
|
+
return body.flatMap((entry) => readPaintingBodies(entry));
|
|
649
|
+
}
|
|
650
|
+
const record = asRecord(body);
|
|
651
|
+
if (!record) return [];
|
|
652
|
+
const url = asString(record.id) ?? asString(record.value);
|
|
653
|
+
return url === null ? [] : [record];
|
|
654
|
+
}
|
|
655
|
+
|
|
645
656
|
/**
|
|
646
|
-
* Reads the slide media
|
|
657
|
+
* Reads the slide media URLs from the canvas's painting annotation bodies:
|
|
647
658
|
* typed bodies contribute their `id`, TextualBody (HTML) content its `value`
|
|
648
659
|
* - the legacy format stores both in `media.url`. Also returns the region of
|
|
649
660
|
* a selector on the annotation target, and the body fields listed on
|
|
650
661
|
* {@link PaintingBody}.
|
|
662
|
+
*
|
|
663
|
+
* Every painting match is collected in order (#358): the first becomes the
|
|
664
|
+
* slide's `media`, further ones its `media_extra`.
|
|
651
665
|
*/
|
|
652
|
-
function
|
|
666
|
+
function readPaintings(
|
|
653
667
|
canvas: Record<string, unknown>,
|
|
654
668
|
width: number | null = null,
|
|
655
669
|
height: number | null = null,
|
|
656
|
-
): PaintingBody
|
|
670
|
+
): PaintingBody[] {
|
|
671
|
+
const found: PaintingBody[] = [];
|
|
672
|
+
const annotationPages = Array.isArray(canvas.items) ? canvas.items : [];
|
|
673
|
+
for (const page of annotationPages) {
|
|
674
|
+
const pageRecord = asRecord(page);
|
|
675
|
+
if (!pageRecord) continue;
|
|
676
|
+
const annotations = Array.isArray(pageRecord.items) ? pageRecord.items : [];
|
|
677
|
+
for (const annotation of annotations) {
|
|
678
|
+
const annotationRecord = asRecord(annotation);
|
|
679
|
+
if (!annotationRecord) continue;
|
|
680
|
+
// motivation may be an array (notably ["painting","supplementing"]):
|
|
681
|
+
// accept when any entry names painting. An absent or unparsable
|
|
682
|
+
// motivation stays lenient (treated as painting), as before — only
|
|
683
|
+
// the array form is new.
|
|
684
|
+
const rawMotivation = annotationRecord.motivation;
|
|
685
|
+
const motivations = Array.isArray(rawMotivation) ? rawMotivation : [rawMotivation];
|
|
686
|
+
const isPainting = motivations.some((m) => {
|
|
687
|
+
const s = asString(m);
|
|
688
|
+
return s === null || s === "painting";
|
|
689
|
+
});
|
|
690
|
+
if (!isPainting) continue;
|
|
691
|
+
for (const body of readPaintingBodies(annotationRecord.body)) {
|
|
692
|
+
const painting: PaintingBody = {
|
|
693
|
+
url: asString(body.id) ?? asString(body.value) ?? "",
|
|
694
|
+
region: readSelector(annotationRecord.target, width, height).region,
|
|
695
|
+
type: asString(body.type),
|
|
696
|
+
format: asString(body.format),
|
|
697
|
+
// P3 puts label / requiredStatement / accessibilitySummary on
|
|
698
|
+
// the Annotation; some producers put them on the body, so the
|
|
699
|
+
// body is still consulted as a fallback (§2.7)
|
|
700
|
+
label:
|
|
701
|
+
flattenLanguageMap(annotationRecord.label) ||
|
|
702
|
+
flattenLanguageMap(body.label) ||
|
|
703
|
+
null,
|
|
704
|
+
accessibilitySummary:
|
|
705
|
+
flattenLanguageMap(annotationRecord.accessibilitySummary) ||
|
|
706
|
+
flattenLanguageMap(body.accessibilitySummary) ||
|
|
707
|
+
null,
|
|
708
|
+
credit: readAnnotationCredit(annotationRecord) ?? readBodyCredit(body),
|
|
709
|
+
thumbnail: readThumbnailId(body.thumbnail),
|
|
710
|
+
duration: asNumber(body.duration),
|
|
711
|
+
start: asNumber(body.start),
|
|
712
|
+
end: asNumber(body.end),
|
|
713
|
+
subtitles: readBodySubtitles(annotationRecord.body),
|
|
714
|
+
};
|
|
715
|
+
readPaintingPlayback(annotationRecord, painting);
|
|
716
|
+
found.push(painting);
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
return found;
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
/**
|
|
724
|
+
* Reads a slide narration from `motivation: "supplementing"` annotations:
|
|
725
|
+
* the first `Sound`/`Video` body wins. Painting bodies are skipped here —
|
|
726
|
+
* the painting reader owns them, including the array-motivation form — and
|
|
727
|
+
* anything else (commenting, tagging, …) belongs to the annotation stops.
|
|
728
|
+
*
|
|
729
|
+
* Playback flags ride `storymap:` foreign members on the annotation (or the
|
|
730
|
+
* body as a fallback), which need no vocabulary registration.
|
|
731
|
+
*/
|
|
732
|
+
function readSupplementing(canvas: Record<string, unknown>): {
|
|
733
|
+
url: string;
|
|
734
|
+
loop?: boolean;
|
|
735
|
+
offset?: number;
|
|
736
|
+
play?: "auto" | "click";
|
|
737
|
+
stopOnExit?: boolean;
|
|
738
|
+
stopAllPrevious?: boolean;
|
|
739
|
+
} | null {
|
|
657
740
|
const annotationPages = Array.isArray(canvas.items) ? canvas.items : [];
|
|
658
741
|
for (const page of annotationPages) {
|
|
659
742
|
const pageRecord = asRecord(page);
|
|
@@ -662,33 +745,24 @@ function readPainting(
|
|
|
662
745
|
for (const annotation of annotations) {
|
|
663
746
|
const annotationRecord = asRecord(annotation);
|
|
664
747
|
if (!annotationRecord) continue;
|
|
665
|
-
const
|
|
666
|
-
|
|
748
|
+
const rawMotivation = annotationRecord.motivation;
|
|
749
|
+
const motivations = Array.isArray(rawMotivation) ? rawMotivation : [rawMotivation];
|
|
750
|
+
if (!motivations.some((m) => asString(m) === "supplementing")) continue;
|
|
667
751
|
const body = readFirstTypedBody(annotationRecord.body);
|
|
668
752
|
if (body === null) continue;
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
flattenLanguageMap(annotationRecord.accessibilitySummary) ||
|
|
683
|
-
flattenLanguageMap(body.accessibilitySummary) ||
|
|
684
|
-
null,
|
|
685
|
-
credit: readAnnotationCredit(annotationRecord) ?? readBodyCredit(body),
|
|
686
|
-
thumbnail: readThumbnailId(body.thumbnail),
|
|
687
|
-
duration: asNumber(body.duration),
|
|
688
|
-
start: asNumber(body.start),
|
|
689
|
-
end: asNumber(body.end),
|
|
690
|
-
subtitles: readBodySubtitles(annotationRecord.body),
|
|
691
|
-
};
|
|
753
|
+
const type = asString(body.type);
|
|
754
|
+
if (type !== "Sound" && type !== "Video") continue;
|
|
755
|
+
const url = asString(body.id) ?? asString(body.value);
|
|
756
|
+
if (url === null) continue;
|
|
757
|
+
const narration: {
|
|
758
|
+
url: string;
|
|
759
|
+
loop?: boolean;
|
|
760
|
+
offset?: number;
|
|
761
|
+
play?: "auto" | "click";
|
|
762
|
+
stopOnExit?: boolean;
|
|
763
|
+
stopAllPrevious?: boolean;
|
|
764
|
+
} = { url, ...readPlaybackBag(annotationRecord), ...readPlaybackBag(body) };
|
|
765
|
+
return narration;
|
|
692
766
|
}
|
|
693
767
|
}
|
|
694
768
|
return null;
|
|
@@ -728,6 +802,28 @@ function readFeatureLocation(feature: unknown): StorymapSlideLocation | null {
|
|
|
728
802
|
if (value === undefined || value === null || value === "") continue;
|
|
729
803
|
locationProps[key] = value;
|
|
730
804
|
}
|
|
805
|
+
// Narrow the slideshow-driven keys the verbatim copy cannot check:
|
|
806
|
+
// a mistyped value must not reach the map as a wrong-typed field.
|
|
807
|
+
if (typeof location.rotation !== "number" || !Number.isFinite(location.rotation)) {
|
|
808
|
+
delete location.rotation;
|
|
809
|
+
}
|
|
810
|
+
if (typeof location.basemap !== "string" || location.basemap === "") {
|
|
811
|
+
delete location.basemap;
|
|
812
|
+
}
|
|
813
|
+
const filter = asRecord(location.filter);
|
|
814
|
+
if (location.filter !== undefined && filter === null) delete location.filter;
|
|
815
|
+
const mask = asRecord(location.mask);
|
|
816
|
+
if (location.mask !== undefined) {
|
|
817
|
+
if (
|
|
818
|
+
mask === null ||
|
|
819
|
+
typeof mask.x !== "number" ||
|
|
820
|
+
typeof mask.y !== "number" ||
|
|
821
|
+
typeof mask.w !== "number" ||
|
|
822
|
+
typeof mask.h !== "number"
|
|
823
|
+
) {
|
|
824
|
+
delete location.mask;
|
|
825
|
+
}
|
|
826
|
+
}
|
|
731
827
|
}
|
|
732
828
|
return location;
|
|
733
829
|
}
|
|
@@ -956,7 +1052,11 @@ function canvasToSlide(
|
|
|
956
1052
|
// requiredStatement / accessibilitySummary as caption, credit and alt
|
|
957
1053
|
// text (§2.7 — the `mediaCaption`/`mediaCredit`/`mediaAlt` canvas terms are
|
|
958
1054
|
// gone). srcset/sizes remain terms: IIIF has no vocabulary for either.
|
|
959
|
-
const
|
|
1055
|
+
const paintings = readPaintings(record, asNumber(record.width), asNumber(record.height));
|
|
1056
|
+
// the first painting is the slide's media; further ones are its
|
|
1057
|
+
// media_extra (#358)
|
|
1058
|
+
const painting = paintings[0] ?? null;
|
|
1059
|
+
const extraPaintings = paintings.slice(1);
|
|
960
1060
|
const caption = painting?.label ?? null;
|
|
961
1061
|
const credit = painting?.credit ?? null;
|
|
962
1062
|
const alt = painting?.accessibilitySummary ?? null;
|
|
@@ -984,14 +1084,41 @@ function canvasToSlide(
|
|
|
984
1084
|
if (sizes !== null) media.sizes = sizes;
|
|
985
1085
|
if (thumbnail !== null) media.thumb = thumbnail;
|
|
986
1086
|
if (painting?.subtitles != null) media.subtitles = painting.subtitles;
|
|
1087
|
+
if (painting?.offset !== undefined) media.offset = painting.offset;
|
|
1088
|
+
if (painting?.loop !== undefined) media.loop = painting.loop;
|
|
1089
|
+
if (painting?.play !== undefined) media.play = painting.play;
|
|
1090
|
+
if (painting?.stopOnExit !== undefined) media.stopOnExit = painting.stopOnExit;
|
|
987
1091
|
slide.media = media;
|
|
988
1092
|
}
|
|
1093
|
+
if (extraPaintings.length > 0) {
|
|
1094
|
+
slide.media_extra = extraPaintings.map((extra) => {
|
|
1095
|
+
const item: StorymapSlideMedia = { url: extra.url };
|
|
1096
|
+
if (extra.label !== null) item.caption = extra.label;
|
|
1097
|
+
if (extra.credit !== null) item.credit = extra.credit;
|
|
1098
|
+
if (extra.accessibilitySummary !== null) item.alt = extra.accessibilitySummary;
|
|
1099
|
+
return item;
|
|
1100
|
+
});
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
// narration: a supplementing Sound/Video body plays as the slide's
|
|
1104
|
+
// narration (V1 of #358). Skipped when it duplicates a painting URL —
|
|
1105
|
+
// a body carrying both motivations is already the slide's media.
|
|
1106
|
+
const supplement = readSupplementing(record);
|
|
1107
|
+
const mediaUrls = new Set(
|
|
1108
|
+
[slide.media?.url, ...(slide.media_extra ?? []).map((item) => item.url)].filter(
|
|
1109
|
+
(url): url is string => typeof url === "string" && url !== "",
|
|
1110
|
+
),
|
|
1111
|
+
);
|
|
1112
|
+
if (supplement !== null && !isDuplicateNarration([...mediaUrls], supplement.url)) {
|
|
1113
|
+
const { url, ...flags } = supplement;
|
|
1114
|
+
slide.narration = { url, ...flags };
|
|
1115
|
+
}
|
|
989
1116
|
|
|
990
1117
|
// location: canvas navPlace, falling back to a manifest-level navPlace
|
|
991
1118
|
// aggregated in items order (both are allowed by the proposal)
|
|
992
1119
|
const location = readLocation(record.navPlace) ?? readFeatureLocation(manifestFeature);
|
|
993
1120
|
if (location !== null) slide.location = location;
|
|
994
|
-
// IIIF xywh region (
|
|
1121
|
+
// IIIF xywh region (slideshow-style image stops): [x, y, w, h] pixels.
|
|
995
1122
|
// The extension term wins over the interoperable spelling — an Image API
|
|
996
1123
|
// Selector on the painting annotation target
|
|
997
1124
|
// region: the painting annotation's target selector, the only source since
|
|
@@ -1000,6 +1127,72 @@ function canvasToSlide(
|
|
|
1000
1127
|
if (region !== null) {
|
|
1001
1128
|
slide.location = { ...(slide.location ?? {}), region };
|
|
1002
1129
|
}
|
|
1130
|
+
// View directives as canvas `storymap:` terms: the fallback for slides
|
|
1131
|
+
// without lat/lon (no navPlace to carry them). navPlace wins when both
|
|
1132
|
+
// spell the same key.
|
|
1133
|
+
const canvasRotation = record[STORYMAP_PREFIX + "rotation"];
|
|
1134
|
+
if (
|
|
1135
|
+
slide.location?.rotation === undefined &&
|
|
1136
|
+
typeof canvasRotation === "number" &&
|
|
1137
|
+
Number.isFinite(canvasRotation)
|
|
1138
|
+
) {
|
|
1139
|
+
slide.location = { ...(slide.location ?? {}), rotation: canvasRotation };
|
|
1140
|
+
}
|
|
1141
|
+
const canvasBasemap = record[STORYMAP_PREFIX + "basemap"];
|
|
1142
|
+
if (
|
|
1143
|
+
slide.location?.basemap === undefined &&
|
|
1144
|
+
typeof canvasBasemap === "string" &&
|
|
1145
|
+
canvasBasemap !== ""
|
|
1146
|
+
) {
|
|
1147
|
+
slide.location = { ...(slide.location ?? {}), basemap: canvasBasemap };
|
|
1148
|
+
}
|
|
1149
|
+
const canvasFilter = asRecord(record[STORYMAP_PREFIX + "filter"]);
|
|
1150
|
+
if (slide.location?.filter === undefined && canvasFilter !== null) {
|
|
1151
|
+
slide.location = { ...(slide.location ?? {}), filter: canvasFilter };
|
|
1152
|
+
}
|
|
1153
|
+
const canvasMask = asRecord(record[STORYMAP_PREFIX + "mask"]);
|
|
1154
|
+
if (slide.location?.mask === undefined && canvasMask !== null) {
|
|
1155
|
+
const mx = canvasMask.x;
|
|
1156
|
+
const my = canvasMask.y;
|
|
1157
|
+
const mw = canvasMask.w;
|
|
1158
|
+
const mh = canvasMask.h;
|
|
1159
|
+
if (
|
|
1160
|
+
typeof mx === "number" &&
|
|
1161
|
+
typeof my === "number" &&
|
|
1162
|
+
typeof mw === "number" &&
|
|
1163
|
+
typeof mh === "number"
|
|
1164
|
+
) {
|
|
1165
|
+
slide.location = {
|
|
1166
|
+
...(slide.location ?? {}),
|
|
1167
|
+
mask: { ...canvasMask, x: mx, y: my, w: mw, h: mh },
|
|
1168
|
+
};
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
const canvasSlidetimeout = record[STORYMAP_PREFIX + "slidetimeout"];
|
|
1172
|
+
if (
|
|
1173
|
+
slide.slidetimeout === undefined &&
|
|
1174
|
+
typeof canvasSlidetimeout === "number" &&
|
|
1175
|
+
Number.isFinite(canvasSlidetimeout) &&
|
|
1176
|
+
canvasSlidetimeout >= 0
|
|
1177
|
+
) {
|
|
1178
|
+
slide.slidetimeout = canvasSlidetimeout;
|
|
1179
|
+
}
|
|
1180
|
+
const canvasImgoverlay = asRecord(record[STORYMAP_PREFIX + "imgoverlay"]);
|
|
1181
|
+
if (slide.imgoverlay === undefined && canvasImgoverlay !== null) {
|
|
1182
|
+
const overlayUrl = asString(canvasImgoverlay.url);
|
|
1183
|
+
if (overlayUrl !== null) slide.imgoverlay = { ...canvasImgoverlay, url: overlayUrl };
|
|
1184
|
+
}
|
|
1185
|
+
const canvasProvenance = asRecord(record[STORYMAP_PREFIX + "provenance"]);
|
|
1186
|
+
if (slide.provenance === undefined && canvasProvenance !== null) {
|
|
1187
|
+
const provenance: Record<string, unknown> = {};
|
|
1188
|
+
for (const key of ["manifest", "canvas", "image"]) {
|
|
1189
|
+
const value = asString(canvasProvenance[key]);
|
|
1190
|
+
if (value !== null) provenance[key] = value;
|
|
1191
|
+
}
|
|
1192
|
+
if (Object.keys(provenance).length > 0) {
|
|
1193
|
+
slide.provenance = provenance as StorymapSlide["provenance"];
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
1003
1196
|
|
|
1004
1197
|
// StoryMap extension terms
|
|
1005
1198
|
const slideType = asString(record[STORYMAP_PREFIX + "type"]);
|
|
@@ -1027,21 +1220,11 @@ function canvasToSlide(
|
|
|
1027
1220
|
*/
|
|
1028
1221
|
const STOP_MOTIVATIONS = new Set(["commenting", "tagging", "classifying", "describing"]);
|
|
1029
1222
|
|
|
1030
|
-
/** Escapes the five characters that would otherwise be markup, so a
|
|
1031
|
-
* `text/plain` annotation body cannot inject HTML. The renderer sanitizes
|
|
1032
|
-
* slide text as well; this keeps the stored value honest on its own. */
|
|
1033
|
-
function escapeText(value: string): string {
|
|
1034
|
-
return value
|
|
1035
|
-
.replace(/&/g, "&")
|
|
1036
|
-
.replace(/</g, "<")
|
|
1037
|
-
.replace(/>/g, ">")
|
|
1038
|
-
.replace(/"/g, """)
|
|
1039
|
-
.replace(/'/g, "'");
|
|
1040
|
-
}
|
|
1041
|
-
|
|
1042
1223
|
/**
|
|
1043
1224
|
* A `text/plain` body becomes one paragraph per blank-line-separated block,
|
|
1044
|
-
* escaped
|
|
1225
|
+
* escaped (`escapeText` is shared with the slideshow translator, which
|
|
1226
|
+
* wraps single paragraphs the same way). A `text/html` body is passed
|
|
1227
|
+
* through as markup — the slide text
|
|
1045
1228
|
* pipeline (`sanitizeSlideText`, via `media/types/Text.ts`) sanitizes whatever
|
|
1046
1229
|
* it is given, which is the same path a storymap JSON `text.text` takes.
|
|
1047
1230
|
*/
|
|
@@ -1666,8 +1849,15 @@ function applyMapConfig(data: StorymapData, config: Record<string, unknown>): vo
|
|
|
1666
1849
|
if (left !== null && top !== null) data.map_center_offset = { left, top };
|
|
1667
1850
|
}
|
|
1668
1851
|
|
|
1669
|
-
const
|
|
1670
|
-
if (
|
|
1852
|
+
const fontCssRaw = readTerm(config, "fontCss");
|
|
1853
|
+
if (fontCssRaw === false) {
|
|
1854
|
+
// a manifest can disable the injected theme exactly like storymap
|
|
1855
|
+
// JSON with font_css: false (the host bundles the theme itself)
|
|
1856
|
+
data.font_css = false;
|
|
1857
|
+
} else {
|
|
1858
|
+
const fontCss = asString(fontCssRaw);
|
|
1859
|
+
if (fontCss !== null) data.font_css = fontCss;
|
|
1860
|
+
}
|
|
1671
1861
|
|
|
1672
1862
|
const callToAction = asBoolean(readTerm(config, "callToAction"));
|
|
1673
1863
|
if (callToAction !== null) data.call_to_action = callToAction;
|
|
@@ -1731,9 +1921,8 @@ function applyMapConfig(data: StorymapData, config: Record<string, unknown>): vo
|
|
|
1731
1921
|
|
|
1732
1922
|
/** Reads a `[west, south, east, north]` lon/lat box of four finite numbers. */
|
|
1733
1923
|
function readLonLatBox(value: unknown): [number, number, number, number] | null {
|
|
1734
|
-
if (!
|
|
1735
|
-
|
|
1736
|
-
const [west, south, east, north] = value as number[];
|
|
1924
|
+
if (!isLonLatBox(value)) return null;
|
|
1925
|
+
const [west, south, east, north] = value;
|
|
1737
1926
|
if (east <= west || north <= south) return null;
|
|
1738
1927
|
return [west, south, east, north];
|
|
1739
1928
|
}
|