The ambient player: a second
Whether the ambient bed is actually playing: HTMLMediaElement.paused
cannot be trusted here (test doubles and pre-gesture elements report
stale state), so the viewer tracks it explicitly.
The advance armed for the current slide (media-ended or the timer).
the data source was a IIIF Presentation manifest (legacy zoomify options are ignored)
the data source was a slideshow tour (slideshow-only features apply)
Set by dispose() so a second call returns early.
The deep link the page was opened with, captured before anything can rewrite the URL.
This has to be read early. The slider's opening goTo() can reach
_navigate() before _onLoaded() runs, and _navigate() calls
_syncHash() — which would overwrite the incoming link with the current
slide before _onLoaded() ever looked at it. Whether that happens
depends on the order the slider and map finish loading, which is exactly
the kind of race that makes a deep link work most of the time.
bumped on interaction, so page-wide keys can pick one viewer
identity of this viewer's claim on the page-wide locale
the locale the caller actually asked for, if any; see _loadLanguage
map_type: "none": the story is text-and-media, with no map at all.
Whether narration consent was granted (asked once per story).
The narration player: one
What each player is (or was) playing, so same-URL beds continue.
Guards a stale play() from a slide change that already moved on.
Reduced-motion toggles mid-story stop the advance timer.
Stored so dispose() can remove them again (see AGENTS: no leaks)
The manifest this viewer was built from, kept so loadAnnotations() can
resolve its seeAlso targets. null for a storymap-JSON viewer, which
has no seeAlso.
A narration that was skipped for want of a gesture, to retry later.
True once the visitor has clicked/keyed: unmuted audio needs this.
The raw OpenLayers map. null until the map is built (the
constructor assigns it during data load) — check it, or use the
accessors, which return null until then.
StaticSCRIPT_The library base path (the directory containing the module).
Derived from import.meta.url, which works both for the source module
(src/main.ts) and the built ESM bundle (js/storymap.js).
Internal
This viewer's root element. Satisfies the Participant shape the
page-wide key registry (src/core/viewers.ts) arbitrates on.
Internal
Monotonic stamp of the last interaction with this viewer; the highest one owns a page-wide keypress when nothing is focused.
Basemap candidates compatible with the live view. A runtime basemap
swap rebuilds layers but never the view, so an image-space candidate
(iiif with map_as_image) on a mercator view — or the reverse —
would wrap the wrong projection around the tiles: markers land in the
wrong place and zoom is meaningless. Refused candidates are dropped
with a warning, like the existing setMapOptions declines.
Publish the slideshow-driven player chrome as container state. Everything here is opt-in: unset options leave the container exactly as the long-standing layout does, so existing stories are unaffected.
A #slide-… hash or an iiif-content parameter deep-links the storymap
(applied on load + hashchange).
The hash token is an index when it is all digits and a uniqueid
otherwise, which is unambiguous because an index never contains anything
else. The index form stays supported, so links shared before ids were
emitted keep working; they just stop being stable the moment a slide is
inserted.
Optionalcaptured: { hash: string; state: ContentState | null } | nullRepaint everything a runtime language switch affects: the menubar
labels and the vco-rtl class.
This deliberately does not call updateDisplay(). A full re-layout
re-runs the slider's goTo(current_slide), which goes through the
navigation path that re-arms autoplay — so calling it here silently
cancelled a pending autoplay tick and the story stopped advancing.
Publish the text colour custom properties. Called from both the initial layout and every runtime option change (issue #177) so the two paths cannot drift.
Announce a slide transition: transitionstart now (with the glide
duration both animations were started with) and transitionend once
it elapses. Restarts on every navigation, so only the latest
transition ever ends.
The current slide as a Content State, or null when it has no id.
The current slide's uniqueid, or null when the data carries none.
A storymap-JSON slide usually has no uniqueid — the slider generates
one per slide for its own DOM ids, but that is not a shareable identity,
so it is deliberately not used here. A IIIF manifest supplies one per
canvas (§2.3), and that is what a deep link should name.
Follow a tour's first.next page chain, returning the following pages
for the translator. Bounded (20) and cycle-guarded; a broken chain
reports incompletely so the caller warns instead of truncating
silently. Relative links resolve against the tour URL.
The URL is ours to own as soon as the story is: the deep link is read and the hash is kept in step.
This deliberately does not wait for the map. The map only reports
itself loaded on OpenLayers' first loadend, which is the first paint of
the base tiles — so a deep link used to be applied only once
tile.openstreetmap.org answered, and on a slow or unreachable tile
host it was never applied at all. Nothing about a URL depends on imagery:
the data and the slider are what it needs, and both are ready before the
map is. _applyHashSlide → goTo → _navigate already tolerates a map
that has not reported loaded, and the map re-syncs when it does.
True when the slide's media element exists and is currently playing (not paused, not ended, not merely loadable).
The layer switcher rows: allowed basemaps first, then built overlays. Empty while there is nothing to switch (mapless story, consent-denied tiles), in which case the control hides itself.
The map pane, for the code paths that only run when there is a map.
_el.map is null exactly when the engine is; see _map_required().
The map engine, for the code paths that only run when there is a map.
Throws rather than returning null: reaching one of these from a mapless
story is a bug in this class, and a loud one, rather than a layout that
quietly misbehaves. Public entry points use this._map?. instead.
Reset every slideshow-only option to its long-standing default unless the loaded tour came from a slideshow source. Internal documents keep the pre-existing rendering even when they (or a host, via setMapOptions) name the new keys; slideshow tours keep what the translator emitted. Runs on every load and every runtime option change, over the viewer options and (at runtime) the map's own options copy.
A gesture has happened, so unmuted audio is allowed from now on.
Optionale: unknownOptionale: unknownToggle the native fullscreen API on the storymap container and keep the menubar button state in sync.
Optionale: unknownOptionale: unknownThe slide panel share of the width (side docks) or height (bottom
dock) as a fraction: textsize percent when valid (10..80), else the
built-in split (half for sides, 40% height for the bottom dock).
Play the current slide's narration, if it has one.
One <audio> element is reused for the whole story rather than a
player per slide, so switching slides cannot leave a recording playing
behind. Two things gate it:
media:narration service is requested at start-up
(see _startConsentAsk); a denied or unanswered service means no
playback, and the story still works.The locale this viewer was actually asked for, or undefined when the caller and the document both stayed silent.
A content state or hash token as a slide index, or null when it names nothing. A content state wins over the hash, being the standard spelling.
Arm autoplay for the current slide.
With autoplay_media on and a slide whose media is playable audio or
video, the advance waits for that media to end instead of using the
autoplay millisecond timer. The timer is kept as a fallback in
that case too: a media element that never fires ended (a stalled
fetch, a codec the browser cannot play, or a slide whose media was
never loaded because the visitor skipped past it) would otherwise stop
the story dead. So both are armed, whichever fires first advances, and
the other is cleared.
Seek to a start offset in seconds (slideshow audio.offset):
immediately when the metadata is already there, otherwise once on
loadedmetadata. Guarded by the narration token, so a quick
pass-through cannot seek a newer track.
A canvas id as a slide index, or null.
Start-of-story consent (issue: allow all / individually / decline
all): with consent_required, one dialog lists every external
service the story uses — map tiles, external web fonts and the media
services in the slides. Shown only while some service is unanswered;
per-service slide asks remain the fallback.
Start (or arm) a narration track: loop flag, start offset once the
metadata is available, and play: "click" arms paused for the
visitor instead of playing (see playNarration()). A rejected play()
(policy, network) must not surface as an unhandled rejection.
Stop other slides' ambient audio beds when the new slide claims the
audio: its narration (unless opted out via stopAllPrevious: false),
or playable media that is actually playing (a paused click-to-play
bed must not kill an inherited ambient track it never starts).
A silent slide inherits a playing bed.
Keep the URL in sync with the current slide: the #slide-… hash and,
when the slide has a shareable id, an iiif-content parameter (§5.1).
The query string is rewritten rather than replaced, because pages like
the embed player and the test harness carry their own configuration in
it (?url=…, ?example=…, ?manifest=…).
Whether deferred tile layers may attach (no consent gate, or granted).
Optionalmap_height: numberOptionalanimate: booleanOptionald: numberCompute and display the great-circle distance of the marker route
(issue #341); hidden when show_distance is off.
One console.warn per warning kind: count, detail and sample ids.
Translation losses are loud exactly once, never per slide. Shared
formatting with the CLI (see formatSlideshowWarning).
Close one marker's popup card (n given) or every open card
(omitted). No-op when the viewer is disposed or mapless. Fires
popupclose per card actually closed.
Optionaln: numberRebuild the minimap (OverviewMap control). The constructor already
builds it; this is for hosts that recreate it after a map_type swap
or a deferred tile-consent grant.
Release everything the viewer attached to the page: window/document listeners, timers, the resize observer, running Web Animations, the slider, the menubar, the map and the map markers. Use this when tearing a storymap down (SPA route change, modal close).
Teardown is terminal: the host container is emptied, map becomes
null, and the public methods (goTo, setMapOptions,
createMiniMap, ...) become no-ops rather than throwing. There is no
re-init path; build a new StoryMap on the same element instead.
Calling dispose() twice is a no-op.
The map markers, indexed by slide.
The minimap's OpenLayers map (the OverviewMap control's inner map).
Number of stacked overlay layers (see the overlays option).
The uniqueid of a slide — the current one by default.
A IIIF manifest supplies one per canvas (§2.3) and it is the only
identity a link can be built on, since an index moves when a slide is
inserted. A storymap-JSON slide usually has none, in which case this is
null rather than a generated id: a random one would be stable to nobody.
Optionalindex: numberNavigate to a slide by index. Out-of-range indices are ignored, as is the current index.
Zero-based slide index; the map animates to the slide's
marker. The overview is selected by the slide's own
type: "overview", not by its index.
True when the map shows a IIIF image as a picture of the world
(map_as_image) rather than as a georeferenced map. In that mode the
view is EPSG:4326 image space: map.getView().getCenter() is in
degrees, the resolution is in pixels per degree, and marker locations
are pixel offsets into the image. Ask this instead of checking
map_type — georeferenced IIIF (map_bbox without map_as_image)
is an ordinary mercator map.
Whether slide n's marker card is currently open.
Read the annotation documents this manifest points at with seeAlso and
add the tour stops they carry.
Explicit and asynchronous on purpose (§5.2). The alternative — following
seeAlso inside manifestToStorymapData — would mean one blocking
network round trip per referenced document before the viewer could paint
its first slide, against third-party servers the host may not want to
contact at all. Here the story stands on its own immediately and the
annotations arrive afterwards, behind a per-(id, type) cache, and
annotationsloaded fires when they are in.
The stops are appended, not placed after the canvas they annotate. A canvas's own annotations are inserted in position at parse time; doing the same to a live tour would mean rebuilding the slider and every piece of state that hangs off it, for an ordering nicety. A host that needs the stops placed exactly can read them off the resolved value and place them itself.
OptionalfetchImpl?: {the stops that were added, keyed by canvas id, plus any
SearchService1 endpoint seen and any document that failed to load.
Optionalcontext: unknownOpen the popup card for slide n's marker, navigating there first
when it is not the current slide.
Returns false (and changes nothing) when the viewer is disposed,
has no map (map_type: "none"), n is out of range, or the marker
has popups disabled. Navigating fires the usual change (and
transition) events; a successful open fires popupopen.
Start the current slide's click-armed narration (narration.play: "click"): the control for visitors (or hosts) to start a track that
waits for them. Auto-armed tracks play on their own. No-op when
nothing is armed. Prefers the current slide's own track: with an
ambient bed running, the foreground it armed is the one to start.
Append attribution fragments and refresh the credit line (e.g. for a
custom layer added through tile_source_factory).
Sugar over setMapOptions — setMapOption(name, value) is the
same call as setMapOptions({ [name]: value }).
Change several map options at runtime (see setMapOption).
Set a stacked overlay's opacity by index (see the overlays option).
Alias: getOverlayLayer(index)?.setOpacity(opacity).
Show or hide a stacked overlay by index (see the overlays option).
Prefer getOverlayLayer(index).setVisible(v) on the layer itself; this
wrapper also re-syncs the attribution line and the overlay blend mode.
Re-measure the container and re-layout the map, slider and menubar.
Called automatically on resize (see the trackResize option).
The interactive StoryMap viewer component.
See
StoryMap.constructor for the constructor signature.