@projektemacher/storymapjs 0.10.7 → 0.10.8

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.
Files changed (532) hide show
  1. package/CHANGELOG +350 -19
  2. package/README.md +43 -9
  3. package/dist/assets/demo-DBpeF31K.js +1 -0
  4. package/dist/assets/harness-Cr0rYpvt.js +1 -0
  5. package/dist/assets/harness-multi-CYHgI0a9.js +1 -0
  6. package/dist/assets/harness-support-Do7sIp2j.js +1 -0
  7. package/dist/assets/index-wHPqGrBS.js +1 -0
  8. package/dist/assets/main-DXalctuR.js +17 -0
  9. package/dist/context.json +12 -9
  10. package/dist/css/fonts/files/amatic-sc-cyrillic-400-normal.woff2 +0 -0
  11. package/dist/css/fonts/files/amatic-sc-cyrillic-700-normal.woff2 +0 -0
  12. package/dist/css/fonts/files/amatic-sc-hebrew-400-normal.woff2 +0 -0
  13. package/dist/css/fonts/files/amatic-sc-hebrew-700-normal.woff2 +0 -0
  14. package/dist/css/fonts/files/amatic-sc-vietnamese-400-normal.woff2 +0 -0
  15. package/dist/css/fonts/files/amatic-sc-vietnamese-700-normal.woff2 +0 -0
  16. package/dist/css/fonts/files/bitter-cyrillic-100-italic.woff2 +0 -0
  17. package/dist/css/fonts/files/bitter-cyrillic-100-normal.woff2 +0 -0
  18. package/dist/css/fonts/files/bitter-cyrillic-200-italic.woff2 +0 -0
  19. package/dist/css/fonts/files/bitter-cyrillic-200-normal.woff2 +0 -0
  20. package/dist/css/fonts/files/bitter-cyrillic-300-italic.woff2 +0 -0
  21. package/dist/css/fonts/files/bitter-cyrillic-300-normal.woff2 +0 -0
  22. package/dist/css/fonts/files/bitter-cyrillic-400-italic.woff2 +0 -0
  23. package/dist/css/fonts/files/bitter-cyrillic-400-normal.woff2 +0 -0
  24. package/dist/css/fonts/files/bitter-cyrillic-500-italic.woff2 +0 -0
  25. package/dist/css/fonts/files/bitter-cyrillic-500-normal.woff2 +0 -0
  26. package/dist/css/fonts/files/bitter-cyrillic-600-italic.woff2 +0 -0
  27. package/dist/css/fonts/files/bitter-cyrillic-600-normal.woff2 +0 -0
  28. package/dist/css/fonts/files/bitter-cyrillic-700-italic.woff2 +0 -0
  29. package/dist/css/fonts/files/bitter-cyrillic-700-normal.woff2 +0 -0
  30. package/dist/css/fonts/files/bitter-cyrillic-800-italic.woff2 +0 -0
  31. package/dist/css/fonts/files/bitter-cyrillic-800-normal.woff2 +0 -0
  32. package/dist/css/fonts/files/bitter-cyrillic-900-italic.woff2 +0 -0
  33. package/dist/css/fonts/files/bitter-cyrillic-900-normal.woff2 +0 -0
  34. package/dist/css/fonts/files/bitter-cyrillic-ext-100-italic.woff2 +0 -0
  35. package/dist/css/fonts/files/bitter-cyrillic-ext-100-normal.woff2 +0 -0
  36. package/dist/css/fonts/files/bitter-cyrillic-ext-200-italic.woff2 +0 -0
  37. package/dist/css/fonts/files/bitter-cyrillic-ext-200-normal.woff2 +0 -0
  38. package/dist/css/fonts/files/bitter-cyrillic-ext-300-italic.woff2 +0 -0
  39. package/dist/css/fonts/files/bitter-cyrillic-ext-300-normal.woff2 +0 -0
  40. package/dist/css/fonts/files/bitter-cyrillic-ext-400-italic.woff2 +0 -0
  41. package/dist/css/fonts/files/bitter-cyrillic-ext-400-normal.woff2 +0 -0
  42. package/dist/css/fonts/files/bitter-cyrillic-ext-500-italic.woff2 +0 -0
  43. package/dist/css/fonts/files/bitter-cyrillic-ext-500-normal.woff2 +0 -0
  44. package/dist/css/fonts/files/bitter-cyrillic-ext-600-italic.woff2 +0 -0
  45. package/dist/css/fonts/files/bitter-cyrillic-ext-600-normal.woff2 +0 -0
  46. package/dist/css/fonts/files/bitter-cyrillic-ext-700-italic.woff2 +0 -0
  47. package/dist/css/fonts/files/bitter-cyrillic-ext-700-normal.woff2 +0 -0
  48. package/dist/css/fonts/files/bitter-cyrillic-ext-800-italic.woff2 +0 -0
  49. package/dist/css/fonts/files/bitter-cyrillic-ext-800-normal.woff2 +0 -0
  50. package/dist/css/fonts/files/bitter-cyrillic-ext-900-italic.woff2 +0 -0
  51. package/dist/css/fonts/files/bitter-cyrillic-ext-900-normal.woff2 +0 -0
  52. package/dist/css/fonts/files/bitter-cyrillic-ext-wght-italic.woff2 +0 -0
  53. package/dist/css/fonts/files/bitter-cyrillic-ext-wght-normal.woff2 +0 -0
  54. package/dist/css/fonts/files/bitter-cyrillic-wght-italic.woff2 +0 -0
  55. package/dist/css/fonts/files/bitter-cyrillic-wght-normal.woff2 +0 -0
  56. package/dist/css/fonts/files/bitter-vietnamese-100-italic.woff2 +0 -0
  57. package/dist/css/fonts/files/bitter-vietnamese-100-normal.woff2 +0 -0
  58. package/dist/css/fonts/files/bitter-vietnamese-200-italic.woff2 +0 -0
  59. package/dist/css/fonts/files/bitter-vietnamese-200-normal.woff2 +0 -0
  60. package/dist/css/fonts/files/bitter-vietnamese-300-italic.woff2 +0 -0
  61. package/dist/css/fonts/files/bitter-vietnamese-300-normal.woff2 +0 -0
  62. package/dist/css/fonts/files/bitter-vietnamese-400-italic.woff2 +0 -0
  63. package/dist/css/fonts/files/bitter-vietnamese-400-normal.woff2 +0 -0
  64. package/dist/css/fonts/files/bitter-vietnamese-500-italic.woff2 +0 -0
  65. package/dist/css/fonts/files/bitter-vietnamese-500-normal.woff2 +0 -0
  66. package/dist/css/fonts/files/bitter-vietnamese-600-italic.woff2 +0 -0
  67. package/dist/css/fonts/files/bitter-vietnamese-600-normal.woff2 +0 -0
  68. package/dist/css/fonts/files/bitter-vietnamese-700-italic.woff2 +0 -0
  69. package/dist/css/fonts/files/bitter-vietnamese-700-normal.woff2 +0 -0
  70. package/dist/css/fonts/files/bitter-vietnamese-800-italic.woff2 +0 -0
  71. package/dist/css/fonts/files/bitter-vietnamese-800-normal.woff2 +0 -0
  72. package/dist/css/fonts/files/bitter-vietnamese-900-italic.woff2 +0 -0
  73. package/dist/css/fonts/files/bitter-vietnamese-900-normal.woff2 +0 -0
  74. package/dist/css/fonts/files/bitter-vietnamese-wght-italic.woff2 +0 -0
  75. package/dist/css/fonts/files/bitter-vietnamese-wght-normal.woff2 +0 -0
  76. package/dist/css/fonts/files/cairo-arabic-200-normal.woff2 +0 -0
  77. package/dist/css/fonts/files/cairo-arabic-300-normal.woff2 +0 -0
  78. package/dist/css/fonts/files/cairo-arabic-400-normal.woff2 +0 -0
  79. package/dist/css/fonts/files/cairo-arabic-500-normal.woff2 +0 -0
  80. package/dist/css/fonts/files/cairo-arabic-600-normal.woff2 +0 -0
  81. package/dist/css/fonts/files/cairo-arabic-700-normal.woff2 +0 -0
  82. package/dist/css/fonts/files/cairo-arabic-800-normal.woff2 +0 -0
  83. package/dist/css/fonts/files/cairo-arabic-900-normal.woff2 +0 -0
  84. package/dist/css/fonts/files/cairo-latin-200-normal.woff2 +0 -0
  85. package/dist/css/fonts/files/cairo-latin-300-normal.woff2 +0 -0
  86. package/dist/css/fonts/files/cairo-latin-400-normal.woff2 +0 -0
  87. package/dist/css/fonts/files/cairo-latin-500-normal.woff2 +0 -0
  88. package/dist/css/fonts/files/cairo-latin-600-normal.woff2 +0 -0
  89. package/dist/css/fonts/files/cairo-latin-700-normal.woff2 +0 -0
  90. package/dist/css/fonts/files/cairo-latin-800-normal.woff2 +0 -0
  91. package/dist/css/fonts/files/cairo-latin-900-normal.woff2 +0 -0
  92. package/dist/css/fonts/files/cairo-latin-ext-200-normal.woff2 +0 -0
  93. package/dist/css/fonts/files/cairo-latin-ext-300-normal.woff2 +0 -0
  94. package/dist/css/fonts/files/cairo-latin-ext-400-normal.woff2 +0 -0
  95. package/dist/css/fonts/files/cairo-latin-ext-500-normal.woff2 +0 -0
  96. package/dist/css/fonts/files/cairo-latin-ext-600-normal.woff2 +0 -0
  97. package/dist/css/fonts/files/cairo-latin-ext-700-normal.woff2 +0 -0
  98. package/dist/css/fonts/files/cairo-latin-ext-800-normal.woff2 +0 -0
  99. package/dist/css/fonts/files/cairo-latin-ext-900-normal.woff2 +0 -0
  100. package/dist/css/fonts/files/dancing-script-vietnamese-wght-normal.woff2 +0 -0
  101. package/dist/css/fonts/files/eb-garamond-cyrillic-ext-wght-italic.woff2 +0 -0
  102. package/dist/css/fonts/files/eb-garamond-cyrillic-ext-wght-normal.woff2 +0 -0
  103. package/dist/css/fonts/files/eb-garamond-cyrillic-wght-italic.woff2 +0 -0
  104. package/dist/css/fonts/files/eb-garamond-cyrillic-wght-normal.woff2 +0 -0
  105. package/dist/css/fonts/files/eb-garamond-greek-ext-wght-italic.woff2 +0 -0
  106. package/dist/css/fonts/files/eb-garamond-greek-ext-wght-normal.woff2 +0 -0
  107. package/dist/css/fonts/files/eb-garamond-greek-wght-italic.woff2 +0 -0
  108. package/dist/css/fonts/files/eb-garamond-greek-wght-normal.woff2 +0 -0
  109. package/dist/css/fonts/files/eb-garamond-vietnamese-wght-italic.woff2 +0 -0
  110. package/dist/css/fonts/files/eb-garamond-vietnamese-wght-normal.woff2 +0 -0
  111. package/dist/css/fonts/files/gentium-book-plus-cyrillic-400-italic.woff2 +0 -0
  112. package/dist/css/fonts/files/gentium-book-plus-cyrillic-400-normal.woff2 +0 -0
  113. package/dist/css/fonts/files/gentium-book-plus-cyrillic-700-italic.woff2 +0 -0
  114. package/dist/css/fonts/files/gentium-book-plus-cyrillic-700-normal.woff2 +0 -0
  115. package/dist/css/fonts/files/gentium-book-plus-cyrillic-ext-400-italic.woff2 +0 -0
  116. package/dist/css/fonts/files/gentium-book-plus-cyrillic-ext-400-normal.woff2 +0 -0
  117. package/dist/css/fonts/files/gentium-book-plus-cyrillic-ext-700-italic.woff2 +0 -0
  118. package/dist/css/fonts/files/gentium-book-plus-cyrillic-ext-700-normal.woff2 +0 -0
  119. package/dist/css/fonts/files/gentium-book-plus-greek-400-italic.woff2 +0 -0
  120. package/dist/css/fonts/files/gentium-book-plus-greek-400-normal.woff2 +0 -0
  121. package/dist/css/fonts/files/gentium-book-plus-greek-700-italic.woff2 +0 -0
  122. package/dist/css/fonts/files/gentium-book-plus-greek-700-normal.woff2 +0 -0
  123. package/dist/css/fonts/files/gentium-book-plus-greek-ext-400-italic.woff2 +0 -0
  124. package/dist/css/fonts/files/gentium-book-plus-greek-ext-400-normal.woff2 +0 -0
  125. package/dist/css/fonts/files/gentium-book-plus-greek-ext-700-italic.woff2 +0 -0
  126. package/dist/css/fonts/files/gentium-book-plus-greek-ext-700-normal.woff2 +0 -0
  127. package/dist/css/fonts/files/gentium-book-plus-vietnamese-400-italic.woff2 +0 -0
  128. package/dist/css/fonts/files/gentium-book-plus-vietnamese-400-normal.woff2 +0 -0
  129. package/dist/css/fonts/files/gentium-book-plus-vietnamese-700-italic.woff2 +0 -0
  130. package/dist/css/fonts/files/gentium-book-plus-vietnamese-700-normal.woff2 +0 -0
  131. package/dist/css/fonts/files/old-standard-tt-cyrillic-400-italic.woff2 +0 -0
  132. package/dist/css/fonts/files/old-standard-tt-cyrillic-400-normal.woff2 +0 -0
  133. package/dist/css/fonts/files/old-standard-tt-cyrillic-700-normal.woff2 +0 -0
  134. package/dist/css/fonts/files/old-standard-tt-cyrillic-ext-400-italic.woff2 +0 -0
  135. package/dist/css/fonts/files/old-standard-tt-cyrillic-ext-400-normal.woff2 +0 -0
  136. package/dist/css/fonts/files/old-standard-tt-cyrillic-ext-700-normal.woff2 +0 -0
  137. package/dist/css/fonts/files/old-standard-tt-vietnamese-400-italic.woff2 +0 -0
  138. package/dist/css/fonts/files/old-standard-tt-vietnamese-400-normal.woff2 +0 -0
  139. package/dist/css/fonts/files/old-standard-tt-vietnamese-700-normal.woff2 +0 -0
  140. package/dist/css/fonts/files/open-sans-cyrillic-ext-wght-italic.woff2 +0 -0
  141. package/dist/css/fonts/files/open-sans-cyrillic-ext-wght-normal.woff2 +0 -0
  142. package/dist/css/fonts/files/open-sans-cyrillic-wght-italic.woff2 +0 -0
  143. package/dist/css/fonts/files/open-sans-cyrillic-wght-normal.woff2 +0 -0
  144. package/dist/css/fonts/files/open-sans-greek-ext-wght-italic.woff2 +0 -0
  145. package/dist/css/fonts/files/open-sans-greek-ext-wght-normal.woff2 +0 -0
  146. package/dist/css/fonts/files/open-sans-greek-wght-italic.woff2 +0 -0
  147. package/dist/css/fonts/files/open-sans-greek-wght-normal.woff2 +0 -0
  148. package/dist/css/fonts/files/open-sans-hebrew-wght-italic.woff2 +0 -0
  149. package/dist/css/fonts/files/open-sans-hebrew-wght-normal.woff2 +0 -0
  150. package/dist/css/fonts/files/open-sans-vietnamese-wght-italic.woff2 +0 -0
  151. package/dist/css/fonts/files/open-sans-vietnamese-wght-normal.woff2 +0 -0
  152. package/dist/css/fonts/files/playfair-display-cyrillic-wght-italic.woff2 +0 -0
  153. package/dist/css/fonts/files/playfair-display-cyrillic-wght-normal.woff2 +0 -0
  154. package/dist/css/fonts/files/playfair-display-sc-cyrillic-400-italic.woff2 +0 -0
  155. package/dist/css/fonts/files/playfair-display-sc-cyrillic-400-normal.woff2 +0 -0
  156. package/dist/css/fonts/files/playfair-display-sc-cyrillic-700-italic.woff2 +0 -0
  157. package/dist/css/fonts/files/playfair-display-sc-cyrillic-700-normal.woff2 +0 -0
  158. package/dist/css/fonts/files/playfair-display-sc-cyrillic-900-italic.woff2 +0 -0
  159. package/dist/css/fonts/files/playfair-display-sc-cyrillic-900-normal.woff2 +0 -0
  160. package/dist/css/fonts/files/playfair-display-sc-vietnamese-400-italic.woff2 +0 -0
  161. package/dist/css/fonts/files/playfair-display-sc-vietnamese-400-normal.woff2 +0 -0
  162. package/dist/css/fonts/files/playfair-display-sc-vietnamese-700-italic.woff2 +0 -0
  163. package/dist/css/fonts/files/playfair-display-sc-vietnamese-700-normal.woff2 +0 -0
  164. package/dist/css/fonts/files/playfair-display-sc-vietnamese-900-italic.woff2 +0 -0
  165. package/dist/css/fonts/files/playfair-display-sc-vietnamese-900-normal.woff2 +0 -0
  166. package/dist/css/fonts/files/playfair-display-vietnamese-wght-italic.woff2 +0 -0
  167. package/dist/css/fonts/files/playfair-display-vietnamese-wght-normal.woff2 +0 -0
  168. package/dist/css/fonts/files/pt-sans-cyrillic-400-italic.woff2 +0 -0
  169. package/dist/css/fonts/files/pt-sans-cyrillic-400-normal.woff2 +0 -0
  170. package/dist/css/fonts/files/pt-sans-cyrillic-700-italic.woff2 +0 -0
  171. package/dist/css/fonts/files/pt-sans-cyrillic-700-normal.woff2 +0 -0
  172. package/dist/css/fonts/files/pt-sans-cyrillic-ext-400-italic.woff2 +0 -0
  173. package/dist/css/fonts/files/pt-sans-cyrillic-ext-400-normal.woff2 +0 -0
  174. package/dist/css/fonts/files/pt-sans-cyrillic-ext-700-italic.woff2 +0 -0
  175. package/dist/css/fonts/files/pt-sans-cyrillic-ext-700-normal.woff2 +0 -0
  176. package/dist/css/fonts/files/pt-sans-narrow-cyrillic-400-normal.woff2 +0 -0
  177. package/dist/css/fonts/files/pt-sans-narrow-cyrillic-700-normal.woff2 +0 -0
  178. package/dist/css/fonts/files/pt-sans-narrow-cyrillic-ext-400-normal.woff2 +0 -0
  179. package/dist/css/fonts/files/pt-sans-narrow-cyrillic-ext-700-normal.woff2 +0 -0
  180. package/dist/css/fonts/files/pt-serif-cyrillic-400-italic.woff2 +0 -0
  181. package/dist/css/fonts/files/pt-serif-cyrillic-400-normal.woff2 +0 -0
  182. package/dist/css/fonts/files/pt-serif-cyrillic-700-italic.woff2 +0 -0
  183. package/dist/css/fonts/files/pt-serif-cyrillic-700-normal.woff2 +0 -0
  184. package/dist/css/fonts/files/pt-serif-cyrillic-ext-400-italic.woff2 +0 -0
  185. package/dist/css/fonts/files/pt-serif-cyrillic-ext-400-normal.woff2 +0 -0
  186. package/dist/css/fonts/files/pt-serif-cyrillic-ext-700-italic.woff2 +0 -0
  187. package/dist/css/fonts/files/pt-serif-cyrillic-ext-700-normal.woff2 +0 -0
  188. package/dist/css/fonts/files/raleway-cyrillic-ext-wght-italic.woff2 +0 -0
  189. package/dist/css/fonts/files/raleway-cyrillic-ext-wght-normal.woff2 +0 -0
  190. package/dist/css/fonts/files/raleway-cyrillic-wght-italic.woff2 +0 -0
  191. package/dist/css/fonts/files/raleway-cyrillic-wght-normal.woff2 +0 -0
  192. package/dist/css/fonts/files/raleway-vietnamese-wght-italic.woff2 +0 -0
  193. package/dist/css/fonts/files/raleway-vietnamese-wght-normal.woff2 +0 -0
  194. package/dist/css/fonts/files/roboto-slab-cyrillic-ext-wght-normal.woff2 +0 -0
  195. package/dist/css/fonts/files/roboto-slab-cyrillic-wght-normal.woff2 +0 -0
  196. package/dist/css/fonts/files/roboto-slab-greek-ext-wght-normal.woff2 +0 -0
  197. package/dist/css/fonts/files/roboto-slab-greek-wght-normal.woff2 +0 -0
  198. package/dist/css/fonts/files/roboto-slab-vietnamese-wght-normal.woff2 +0 -0
  199. package/dist/css/fonts/files/ubuntu-cyrillic-300-italic.woff2 +0 -0
  200. package/dist/css/fonts/files/ubuntu-cyrillic-300-normal.woff2 +0 -0
  201. package/dist/css/fonts/files/ubuntu-cyrillic-400-italic.woff2 +0 -0
  202. package/dist/css/fonts/files/ubuntu-cyrillic-400-normal.woff2 +0 -0
  203. package/dist/css/fonts/files/ubuntu-cyrillic-500-italic.woff2 +0 -0
  204. package/dist/css/fonts/files/ubuntu-cyrillic-500-normal.woff2 +0 -0
  205. package/dist/css/fonts/files/ubuntu-cyrillic-700-italic.woff2 +0 -0
  206. package/dist/css/fonts/files/ubuntu-cyrillic-700-normal.woff2 +0 -0
  207. package/dist/css/fonts/files/ubuntu-cyrillic-ext-300-italic.woff2 +0 -0
  208. package/dist/css/fonts/files/ubuntu-cyrillic-ext-300-normal.woff2 +0 -0
  209. package/dist/css/fonts/files/ubuntu-cyrillic-ext-400-italic.woff2 +0 -0
  210. package/dist/css/fonts/files/ubuntu-cyrillic-ext-400-normal.woff2 +0 -0
  211. package/dist/css/fonts/files/ubuntu-cyrillic-ext-500-italic.woff2 +0 -0
  212. package/dist/css/fonts/files/ubuntu-cyrillic-ext-500-normal.woff2 +0 -0
  213. package/dist/css/fonts/files/ubuntu-cyrillic-ext-700-italic.woff2 +0 -0
  214. package/dist/css/fonts/files/ubuntu-cyrillic-ext-700-normal.woff2 +0 -0
  215. package/dist/css/fonts/files/ubuntu-greek-300-italic.woff2 +0 -0
  216. package/dist/css/fonts/files/ubuntu-greek-300-normal.woff2 +0 -0
  217. package/dist/css/fonts/files/ubuntu-greek-400-italic.woff2 +0 -0
  218. package/dist/css/fonts/files/ubuntu-greek-400-normal.woff2 +0 -0
  219. package/dist/css/fonts/files/ubuntu-greek-500-italic.woff2 +0 -0
  220. package/dist/css/fonts/files/ubuntu-greek-500-normal.woff2 +0 -0
  221. package/dist/css/fonts/files/ubuntu-greek-700-italic.woff2 +0 -0
  222. package/dist/css/fonts/files/ubuntu-greek-700-normal.woff2 +0 -0
  223. package/dist/css/fonts/files/ubuntu-greek-ext-300-italic.woff2 +0 -0
  224. package/dist/css/fonts/files/ubuntu-greek-ext-300-normal.woff2 +0 -0
  225. package/dist/css/fonts/files/ubuntu-greek-ext-400-italic.woff2 +0 -0
  226. package/dist/css/fonts/files/ubuntu-greek-ext-400-normal.woff2 +0 -0
  227. package/dist/css/fonts/files/ubuntu-greek-ext-500-italic.woff2 +0 -0
  228. package/dist/css/fonts/files/ubuntu-greek-ext-500-normal.woff2 +0 -0
  229. package/dist/css/fonts/files/ubuntu-greek-ext-700-italic.woff2 +0 -0
  230. package/dist/css/fonts/files/ubuntu-greek-ext-700-normal.woff2 +0 -0
  231. package/dist/css/fonts/files/unica-one-vietnamese-400-normal.woff2 +0 -0
  232. package/dist/css/fonts/files/vollkorn-cyrillic-ext-wght-italic.woff2 +0 -0
  233. package/dist/css/fonts/files/vollkorn-cyrillic-ext-wght-normal.woff2 +0 -0
  234. package/dist/css/fonts/files/vollkorn-cyrillic-wght-italic.woff2 +0 -0
  235. package/dist/css/fonts/files/vollkorn-cyrillic-wght-normal.woff2 +0 -0
  236. package/dist/css/fonts/files/vollkorn-greek-wght-italic.woff2 +0 -0
  237. package/dist/css/fonts/files/vollkorn-greek-wght-normal.woff2 +0 -0
  238. package/dist/css/fonts/files/vollkorn-vietnamese-wght-italic.woff2 +0 -0
  239. package/dist/css/fonts/files/vollkorn-vietnamese-wght-normal.woff2 +0 -0
  240. package/dist/css/fonts/font.abril-droidsans.css +3 -0
  241. package/dist/css/fonts/font.amatic-andika.css +57 -0
  242. package/dist/css/fonts/font.bitter-raleway.css +111 -0
  243. package/dist/css/fonts/font.cairo.css +312 -0
  244. package/dist/css/fonts/font.clicker-garamond.css +93 -0
  245. package/dist/css/fonts/font.dancing-ledger.css +12 -0
  246. package/dist/css/fonts/font.default.css +183 -0
  247. package/dist/css/fonts/font.fjalla-average.css +3 -0
  248. package/dist/css/fonts/font.georgia-helvetica.css +3 -0
  249. package/dist/css/fonts/font.knightlab.css +642 -0
  250. package/dist/css/fonts/font.lustria-lato.css +3 -0
  251. package/dist/css/fonts/font.medula-lato.css +3 -0
  252. package/dist/css/fonts/font.oldstandard.css +84 -0
  253. package/dist/css/fonts/font.opensans-gentiumbook.css +183 -0
  254. package/dist/css/fonts/font.playfair-faunaone.css +39 -0
  255. package/dist/css/fonts/font.playfair.css +147 -0
  256. package/dist/css/fonts/font.pt.css +183 -0
  257. package/dist/css/fonts/font.roboto-megrim.css +3 -0
  258. package/dist/css/fonts/font.rufina-sintony.css +3 -0
  259. package/dist/css/fonts/font.ubuntu.css +291 -0
  260. package/dist/css/fonts/font.unicaone-vollkorn.css +84 -0
  261. package/dist/css/storymap.css +1 -1
  262. package/dist/demo.html +2 -2
  263. package/dist/docs/api/.nojekyll +1 -0
  264. package/dist/docs/api/assets/custom.css +47 -0
  265. package/dist/docs/api/assets/hierarchy.js +1 -0
  266. package/dist/docs/api/assets/highlight.css +57 -0
  267. package/dist/docs/api/assets/icons.js +18 -0
  268. package/dist/docs/api/assets/icons.svg +1 -0
  269. package/dist/docs/api/assets/main.js +60 -0
  270. package/dist/docs/api/assets/navigation.js +1 -0
  271. package/dist/docs/api/assets/search.js +1 -0
  272. package/dist/docs/api/assets/style.css +1648 -0
  273. package/dist/docs/api/classes/StoryMap.html +1604 -0
  274. package/dist/docs/api/functions/MediaType.html +51 -0
  275. package/dist/docs/api/functions/isPresentation3Collection.html +47 -0
  276. package/dist/docs/api/functions/isPresentation3Manifest.html +53 -0
  277. package/dist/docs/api/functions/loadCSS.html +63 -0
  278. package/dist/docs/api/functions/manifestToStorymapData.html +49 -0
  279. package/dist/docs/api/functions/setLanguage.html +54 -0
  280. package/dist/docs/api/functions/storymapToManifest.html +57 -0
  281. package/dist/docs/api/functions/validateStorymap.html +53 -0
  282. package/dist/docs/api/functions/validateStorymapAndReport.html +54 -0
  283. package/dist/docs/api/hierarchy.html +28 -0
  284. package/dist/docs/api/index.html +197 -0
  285. package/dist/docs/api/interfaces/AnimationHandle.html +60 -0
  286. package/dist/docs/api/interfaces/ImagereadyPayload.html +71 -0
  287. package/dist/docs/api/interfaces/LanguageEntry.html +102 -0
  288. package/dist/docs/api/interfaces/LatLngLiteral.html +73 -0
  289. package/dist/docs/api/interfaces/LoadOptions.html +59 -0
  290. package/dist/docs/api/interfaces/OlLayer.html +1400 -0
  291. package/dist/docs/api/interfaces/OlMap.html +1656 -0
  292. package/dist/docs/api/interfaces/OlProjection.html +332 -0
  293. package/dist/docs/api/interfaces/OlSource.html +784 -0
  294. package/dist/docs/api/interfaces/OlTileLayer.html +1497 -0
  295. package/dist/docs/api/interfaces/OlVectorLayer.html +1559 -0
  296. package/dist/docs/api/interfaces/OlView.html +1958 -0
  297. package/dist/docs/api/interfaces/StorymapData.html +71 -0
  298. package/dist/docs/api/interfaces/StorymapDataWrapper.html +60 -0
  299. package/dist/docs/api/interfaces/StorymapDocument.html +68 -0
  300. package/dist/docs/api/interfaces/StorymapError.html +63 -0
  301. package/dist/docs/api/interfaces/StorymapGeoreference.html +93 -0
  302. package/dist/docs/api/interfaces/StorymapGeoreferenceBody.html +74 -0
  303. package/dist/docs/api/interfaces/StorymapGroundControlPoint.html +69 -0
  304. package/dist/docs/api/interfaces/StorymapLanguageMap.html +57 -0
  305. package/dist/docs/api/interfaces/StorymapManifest.html +118 -0
  306. package/dist/docs/api/interfaces/StorymapManifestAnnotation.html +105 -0
  307. package/dist/docs/api/interfaces/StorymapManifestBackground.html +81 -0
  308. package/dist/docs/api/interfaces/StorymapManifestCanvas.html +140 -0
  309. package/dist/docs/api/interfaces/StorymapManifestContentResource.html +97 -0
  310. package/dist/docs/api/interfaces/StorymapManifestFeatureCollection.html +82 -0
  311. package/dist/docs/api/interfaces/StorymapManifestGeoreferencing.html +87 -0
  312. package/dist/docs/api/interfaces/StorymapManifestImageService.html +69 -0
  313. package/dist/docs/api/interfaces/StorymapManifestRange.html +75 -0
  314. package/dist/docs/api/interfaces/StorymapManifestService.html +77 -0
  315. package/dist/docs/api/interfaces/StorymapManifestSpecificResource.html +69 -0
  316. package/dist/docs/api/interfaces/StorymapManifestStatement.html +63 -0
  317. package/dist/docs/api/interfaces/StorymapMapOptions.html +92 -0
  318. package/dist/docs/api/interfaces/StorymapOptions.html +532 -0
  319. package/dist/docs/api/interfaces/StorymapOverlayLayer.html +126 -0
  320. package/dist/docs/api/interfaces/StorymapSlide.html +132 -0
  321. package/dist/docs/api/interfaces/StorymapSlideBackground.html +72 -0
  322. package/dist/docs/api/interfaces/StorymapSlideLocation.html +117 -0
  323. package/dist/docs/api/interfaces/StorymapSlideMarker.html +104 -0
  324. package/dist/docs/api/interfaces/StorymapSlideMedia.html +110 -0
  325. package/dist/docs/api/interfaces/StorymapSlideNarration.html +65 -0
  326. package/dist/docs/api/interfaces/StorymapSlideText.html +72 -0
  327. package/dist/docs/api/interfaces/StorymapTilejson.html +105 -0
  328. package/dist/docs/api/media/es.json +31 -0
  329. package/dist/docs/api/media/migration-from-knightlab.md +359 -0
  330. package/dist/docs/api/modules.html +50 -0
  331. package/dist/docs/api/types/ContentState.html +70 -0
  332. package/dist/docs/api/types/ImagereadyKind.html +34 -0
  333. package/dist/docs/api/types/MediaTypeMatch.html +78 -0
  334. package/dist/docs/api/types/TileSourceFactory.html +53 -0
  335. package/dist/docs/authoring.html +293 -0
  336. package/dist/docs/iiif.html +617 -62
  337. package/dist/docs/migration.html +313 -11
  338. package/dist/docs/readme.html +40 -9
  339. package/dist/embed/index.html +1 -1
  340. package/dist/examples/issue-242-nonlatin.json +55 -0
  341. package/dist/examples/issue-305-start-at-slide-data.json +96 -0
  342. package/dist/examples/issue-358-iframe.json +57 -0
  343. package/dist/examples/issue-iiif-geo.json +32 -0
  344. package/dist/examples/issue-image-alt.json +23 -0
  345. package/dist/examples/issue-image-region.json +42 -0
  346. package/dist/examples/issue-image-srcset.json +24 -0
  347. package/dist/examples/map-area-overlays.json +68 -0
  348. package/dist/examples/no-map.json +44 -0
  349. package/dist/examples-iiif/annotated-image.json +188 -0
  350. package/dist/examples-iiif/bob-boilen.json +11 -5
  351. package/dist/examples-iiif/courbet.json +9 -6
  352. package/dist/examples-iiif/empty.json +8 -4
  353. package/dist/examples-iiif/flickr-drewvigal.json +248 -24
  354. package/dist/examples-iiif/flickr-zach.wise.json +8 -4
  355. package/dist/examples-iiif/georeferenced-layer-unsupported.json +153 -0
  356. package/dist/examples-iiif/georeferenced-layer.json +318 -0
  357. package/dist/examples-iiif/iiif-wellcome.json +9 -6
  358. package/dist/examples-iiif/instagram-chenpamela.json +381 -36
  359. package/dist/examples-iiif/instagram_brentfoster.json +429 -40
  360. package/dist/examples-iiif/instagram_couch.json +261 -26
  361. package/dist/examples-iiif/instagram_joegermuska.json +441 -41
  362. package/dist/examples-iiif/instagram_matteich.json +261 -26
  363. package/dist/examples-iiif/instagram_mirandamulligan.json +261 -26
  364. package/dist/examples-iiif/instagram_raymondmccrea.json +261 -26
  365. package/dist/examples-iiif/instagram_test.json +261 -26
  366. package/dist/examples-iiif/issue-134-no-lines.json +9 -5
  367. package/dist/examples-iiif/issue-177-color-vars.json +9 -5
  368. package/dist/examples-iiif/issue-211-rtl.json +9 -5
  369. package/dist/examples-iiif/issue-242-nonlatin.json +239 -0
  370. package/dist/examples-iiif/issue-272-facebook.json +9 -5
  371. package/dist/examples-iiif/issue-305-start-at-slide-data.json +305 -0
  372. package/dist/examples-iiif/issue-305-start-at-slide.json +9 -5
  373. package/dist/examples-iiif/issue-341-distance.json +9 -5
  374. package/dist/examples-iiif/issue-358-iframe.json +190 -0
  375. package/dist/examples-iiif/issue-360-juxtapose.json +22 -7
  376. package/dist/examples-iiif/issue-368-null-fields.json +9 -5
  377. package/dist/examples-iiif/issue-375-code-lists.json +9 -5
  378. package/dist/examples-iiif/issue-381-dateline.json +9 -5
  379. package/dist/examples-iiif/issue-405-custom-icon.json +203 -0
  380. package/dist/examples-iiif/issue-425-many-slides.json +9 -5
  381. package/dist/examples-iiif/issue-428-background.json +20 -8
  382. package/dist/examples-iiif/issue-435-googlemaps.json +9 -5
  383. package/dist/examples-iiif/issue-437-documentcloud.json +22 -7
  384. package/dist/examples-iiif/issue-451-webp.json +9 -5
  385. package/dist/examples-iiif/issue-455-audio.json +9 -5
  386. package/dist/examples-iiif/issue-480-shorts.json +9 -5
  387. package/dist/examples-iiif/issue-506-marker-sync.json +9 -5
  388. package/dist/examples-iiif/issue-iiif-geo.json +200 -0
  389. package/dist/examples-iiif/issue-image-alt.json +90 -0
  390. package/dist/examples-iiif/issue-image-region.json +203 -0
  391. package/dist/examples-iiif/issue-image-srcset.json +87 -0
  392. package/dist/examples-iiif/jansteen.json +9 -6
  393. package/dist/examples-iiif/katrina.json +197 -41
  394. package/dist/examples-iiif/map-area-overlays.json +289 -0
  395. package/dist/examples-iiif/marktwain.json +255 -58
  396. package/dist/examples-iiif/no-map.json +148 -0
  397. package/dist/examples-iiif/population.json +440 -76
  398. package/dist/examples-iiif/president.json +456 -83
  399. package/dist/examples-iiif/scroll-hint.json +190 -0
  400. package/dist/examples-iiif/seurat.json +113 -22
  401. package/dist/examples-iiif/zoomify-clamp.json +153 -0
  402. package/dist/harness-multi.html +17 -0
  403. package/dist/harness.html +3 -2
  404. package/dist/index.html +16 -10
  405. package/dist/js/storymap.d.ts +1439 -114
  406. package/dist/js/storymap.js +24654 -21936
  407. package/dist/js/storymap.js.map +1 -1
  408. package/dist/navplace-properties.json +40 -0
  409. package/docs/migration-from-knightlab.md +229 -9
  410. package/docs/storymap-as-iiif-manifest.md +559 -135
  411. package/package.json +11 -8
  412. package/schema/storymap.schema.json +431 -78
  413. package/src/animation/easings.ts +8 -1
  414. package/src/animation/tween.ts +186 -0
  415. package/src/core/Browser.ts +6 -5
  416. package/src/core/Load.ts +54 -2
  417. package/src/core/Util.ts +255 -193
  418. package/src/core/mixins.ts +24 -7
  419. package/src/core/viewers.ts +94 -0
  420. package/src/dom/DomEvent.ts +27 -8
  421. package/src/globals.d.ts +8 -0
  422. package/src/language/Language.ts +199 -25
  423. package/src/language/locale/.expected-gaps.json +86 -0
  424. package/src/language/locale/be.json +15 -2
  425. package/src/language/locale/bg.json +15 -2
  426. package/src/language/locale/cs.json +15 -2
  427. package/src/language/locale/de.json +12 -1
  428. package/src/language/locale/el.json +15 -2
  429. package/src/language/locale/en.json +6 -2
  430. package/src/language/locale/es.json +15 -2
  431. package/src/language/locale/et.json +5 -1
  432. package/src/language/locale/fr.json +15 -2
  433. package/src/language/locale/he.json +16 -3
  434. package/src/language/locale/hu.json +15 -2
  435. package/src/language/locale/is.json +5 -1
  436. package/src/language/locale/it.json +15 -2
  437. package/src/language/locale/jp.json +15 -2
  438. package/src/language/locale/ko.json +15 -2
  439. package/src/language/locale/nl.json +15 -2
  440. package/src/language/locale/nn.json +5 -1
  441. package/src/language/locale/no.json +15 -2
  442. package/src/language/locale/pl.json +15 -2
  443. package/src/language/locale/pt.json +15 -2
  444. package/src/language/locale/ru.json +15 -2
  445. package/src/language/locale/sk.json +15 -2
  446. package/src/language/locale/sr.json +5 -1
  447. package/src/language/locale/sv.json +15 -2
  448. package/src/language/locale/tr.json +15 -2
  449. package/src/language/locale/uk.json +15 -2
  450. package/src/language/locale/ur.json +5 -1
  451. package/src/language/locale/zh-cn.json +15 -2
  452. package/src/language/locale/zh-tw.json +15 -2
  453. package/src/main.ts +92 -0
  454. package/src/map/Map.ts +183 -106
  455. package/src/map/MapMarker.ts +28 -5
  456. package/src/map/georeference.ts +325 -0
  457. package/src/map/openlayers/Map.OpenLayers.ts +850 -237
  458. package/src/map/openlayers/MapMarker.OpenLayers.ts +248 -24
  459. package/src/map/openlayers/zoomifyTiles.ts +1 -4
  460. package/src/media/EmbedUtil.ts +308 -24
  461. package/src/media/Media.ts +243 -55
  462. package/src/media/embedId.ts +104 -0
  463. package/src/media/types/Audio.ts +9 -82
  464. package/src/media/types/Blockquote.ts +2 -3
  465. package/src/media/types/DailyMotion.ts +5 -10
  466. package/src/media/types/DocumentCloud.ts +6 -35
  467. package/src/media/types/Facebook.ts +3 -4
  468. package/src/media/types/Flickr.ts +9 -9
  469. package/src/media/types/GoogleDoc.ts +3 -4
  470. package/src/media/types/HtmlMedia.ts +167 -0
  471. package/src/media/types/IFrame.ts +3 -4
  472. package/src/media/types/Image.ts +72 -6
  473. package/src/media/types/Juxtapose.ts +14 -5
  474. package/src/media/types/SoundCloud.ts +28 -6
  475. package/src/media/types/Text.ts +19 -13
  476. package/src/media/types/Twitter.ts +27 -14
  477. package/src/media/types/Video.ts +9 -79
  478. package/src/media/types/Vimeo.ts +10 -10
  479. package/src/media/types/Website.ts +23 -13
  480. package/src/media/types/Wikipedia.ts +18 -8
  481. package/src/media/types/YouTube.ts +71 -39
  482. package/src/scss/Typography.scss +1 -3
  483. package/src/scss/VCO.StoryMap.Dark.scss +18 -6
  484. package/src/scss/VCO.StoryMap.scss +37 -6
  485. package/src/scss/Variables.Dark.scss +3 -2
  486. package/src/scss/Variables.scss +3 -2
  487. package/src/scss/core/Mixins.scss +5 -63
  488. package/src/scss/fonts/font.amatic-andika.scss +3 -0
  489. package/src/scss/fonts/font.bitter-raleway.scss +6 -0
  490. package/src/scss/fonts/font.cairo.scss +79 -0
  491. package/src/scss/fonts/font.clicker-garamond.scss +5 -0
  492. package/src/scss/fonts/font.dancing-ledger.scss +1 -0
  493. package/src/scss/fonts/font.knightlab.scss +14 -0
  494. package/src/scss/fonts/font.oldstandard.scss +6 -0
  495. package/src/scss/fonts/font.opensans-gentiumbook.scss +5 -0
  496. package/src/scss/fonts/font.playfair-faunaone.scss +2 -0
  497. package/src/scss/fonts/font.playfair.scss +4 -0
  498. package/src/scss/fonts/font.pt.scss +6 -0
  499. package/src/scss/fonts/font.ubuntu.scss +4 -0
  500. package/src/scss/fonts/font.unicaone-vollkorn.scss +5 -0
  501. package/src/scss/map/VCO.MapMarker.scss +98 -3
  502. package/src/scss/media/VCO.Media.scss +7 -10
  503. package/src/scss/media/types/VCO.Media.Text.scss +8 -0
  504. package/src/scss/media/types/VCO.Media.Wikipedia.scss +2 -1
  505. package/src/scss/slider/VCO.Slide.scss +13 -3
  506. package/src/scss/slider/VCO.SlideNav.scss +11 -17
  507. package/src/scss/slider/VCO.StorySlider.scss +4 -2
  508. package/src/scss/ui/VCO.Consent.scss +44 -0
  509. package/src/scss/ui/VCO.MenuBar.Button.scss +8 -0
  510. package/src/scss/ui/VCO.Message.scss +2 -5
  511. package/src/site/site.ts +2 -1
  512. package/src/slider/Slide.ts +121 -27
  513. package/src/slider/SlideNav.ts +39 -5
  514. package/src/slider/StorySlider.ts +153 -42
  515. package/src/storymap/Consent.ts +407 -82
  516. package/src/storymap/StoryMap.ts +1192 -182
  517. package/src/storymap/content-state.ts +185 -0
  518. package/src/storymap/iiif.ts +1526 -68
  519. package/src/storymap/to-iiif.ts +921 -0
  520. package/src/storymap/validate.ts +80 -7
  521. package/src/types.ts +223 -26
  522. package/src/ui/MenuBar.ts +98 -45
  523. package/src/ui/Message.ts +9 -0
  524. package/src/ui/Swipable.ts +145 -78
  525. package/dist/assets/demo-D6cTmzn6.js +0 -1
  526. package/dist/assets/harness-BPFlzutp.js +0 -1
  527. package/dist/assets/index-DDsPBPZA.js +0 -1
  528. package/dist/assets/main-Y-MYUSJ4.js +0 -17
  529. package/src/animation/morpheus.d.ts +0 -41
  530. package/src/scss/media/types/VCO.Media.Instagram.scss +0 -7
  531. package/src/scss/media/types/VCO.Media.Profile.scss +0 -5
  532. package/src/ui/Draggable.ts +0 -374
@@ -3,8 +3,10 @@ import View from "ol/View";
3
3
  import type { Control } from "ol/control";
4
4
  import type { Interaction } from "ol/interaction";
5
5
  import { Tile as TileLayer, Vector as VectorLayer } from "ol/layer";
6
+ import Layer from "ol/layer/Layer";
6
7
  import VectorTileLayer from "ol/layer/VectorTile";
7
8
  import { XYZ, OSM, IIIF } from "ol/source";
9
+ import type Source from "ol/source/Source";
8
10
  import VectorSource from "ol/source/Vector";
9
11
  import LineString from "ol/geom/LineString";
10
12
  import Feature from "ol/Feature";
@@ -26,8 +28,21 @@ import Map from "../Map";
26
28
  import OpenLayersMapMarker from "./MapMarker.OpenLayers";
27
29
  import { padCroppedZoomifyTile } from "./zoomifyTiles";
28
30
  import type { LinePoint, ViewToOptions } from "../types";
29
- import type { LatLngLiteral, StorymapSlide, StorymapSlideLocation } from "../../types";
30
- import { consentManagerOf, consentMessage, type ConsentManager } from "../../storymap/Consent";
31
+ import type {
32
+ LatLngLiteral,
33
+ StorymapOverlayLayer,
34
+ StorymapTilejson,
35
+ StorymapSlide,
36
+ StorymapSlideLocation,
37
+ } from "../../types";
38
+ import { fitGeoreference, resolveInfoJsonUrl } from "../georeference";
39
+ import {
40
+ consentManagerOf,
41
+ tileService,
42
+ type ConsentManager,
43
+ type ConsentService,
44
+ } from "../../storymap/Consent";
45
+ import { sanitizeSlideText } from "../../media/EmbedUtil";
31
46
 
32
47
  /* Map.OpenLayers
33
48
  Creates a Map using OpenLayers
@@ -35,6 +50,20 @@ import { consentManagerOf, consentMessage, type ConsentManager } from "../../sto
35
50
 
36
51
  const MAX_ZOOM = 19;
37
52
 
53
+ /** What kind of imagery an `imageready` source carries. */
54
+ export type ImagereadyKind = "iiif" | "zoomify" | "tiles";
55
+
56
+ /**
57
+ * Payload of the `imageready` event: the source that became usable, what
58
+ * kind of imagery it is, and the layer carrying it (`null` when the source
59
+ * outlives its layer).
60
+ */
61
+ export interface ImagereadyPayload {
62
+ source: { getState?(): string };
63
+ kind: ImagereadyKind;
64
+ layer: Layer | null;
65
+ }
66
+
38
67
  /**
39
68
  * Zoom ladder for image-space maps (issue #465): rung 0 shows any image fully
40
69
  * zoomed out, finer rungs reach sub-pixel detail. It keeps view zooms,
@@ -45,24 +74,35 @@ const IMAGE_RESOLUTIONS = Array.from({ length: 25 }, (_, i) => 2 ** (16 - i));
45
74
 
46
75
  export default class OpenLayers extends Map {
47
76
  declare "_map": OlMap;
48
- declare "_tile_layer": TileLayer;
77
+ declare "_tile_layer": Layer;
49
78
  declare "_line": VectorLayer;
50
79
  declare "_line_active": VectorLayer;
51
- declare "_tile_layer_mini": TileLayer | null;
80
+ declare "_tile_layer_mini": Layer | null;
52
81
  declare "_mini_map": OverviewMap;
53
82
  declare "_markers": OpenLayersMapMarker[];
54
83
  /** App-level stacked overlays (see the `overlays` option) */
55
- declare "_overlay_layers": TileLayer[];
84
+ declare "_overlay_layers": Layer[];
85
+ /**
86
+ * The `overlays[]` entries that produced a layer, parallel to
87
+ * `_overlay_layers`: a malformed entry is skipped, so the two arrays
88
+ * can be shorter than the option list.
89
+ */
90
+ declare "_overlay_entries": StorymapOverlayLayer[];
56
91
  /** rAF handle of the running active-line draw animation */
57
92
  declare "_line_animation": number | null;
93
+ /** Sources `imageready` has already been fired for. */
94
+ declare "_imageready_fired": WeakSet<object>;
58
95
 
59
96
  /* Create the Map
60
97
  ================================================== */
61
98
  _createMap(): void {
62
- const is_image_map = this.options.map_type === "iiif" && this.options.map_as_image;
99
+ const is_image_map = this.isImageSpace();
100
+ this._imageready_fired = new WeakSet();
63
101
 
64
- // Caller-supplied OpenLayers options: controls/interactions replace the
65
- // defaults, view merges over the computed default, other options pass through
102
+ // Caller-supplied OpenLayers options: `controls` replaces the defaults
103
+ // (the viewer installs none), `interactions` are added to the viewer's
104
+ // own pan/zoom set, `view` merges over the computed default, everything
105
+ // else passes through
66
106
  const user_map_options = this.options.map_options ?? {};
67
107
  const { element: _element, view: user_view, ...passthrough } = user_map_options;
68
108
  const user_view_options = (user_view ?? {}) as Record<string, unknown>;
@@ -76,6 +116,16 @@ export default class OpenLayers extends Map {
76
116
  // space maps use raw pixel coordinates.
77
117
  const bbox_extent = this._bboxExtent();
78
118
 
119
+ // A TileJSON tile source states its own zoom ladder, its covered area
120
+ // and its default view, and all three are honoured here (§2.9). A
121
+ // keyword basemap has none of these, so the defaults stand.
122
+ const tilejson = this._tilejsonFor(this.options.map_type);
123
+ const tile_min_zoom = tilejson?.minzoom ?? 0;
124
+ const tile_max_zoom = tilejson?.maxzoom ?? MAX_ZOOM;
125
+ const tile_extent = tilejson?.bounds
126
+ ? fromLonLat(tilejson.bounds as [number, number, number, number])
127
+ : null;
128
+
79
129
  this._map = new OlMap({
80
130
  ...passthrough,
81
131
  target: this._el.map,
@@ -83,10 +133,19 @@ export default class OpenLayers extends Map {
83
133
  interactions: (user_map_options.interactions as Interaction[]) ?? [],
84
134
  view: new View({
85
135
  projection: is_image_map ? "EPSG:4326" : "EPSG:3857",
86
- center: [0, 0],
87
- zoom: 0,
88
- minZoom: 0,
89
- maxZoom: is_image_map ? IMAGE_RESOLUTIONS.length - 1 : MAX_ZOOM,
136
+ ...(tilejson?.center
137
+ ? {
138
+ center: fromLonLat([tilejson.center[0], tilejson.center[1]]),
139
+ zoom: tilejson.center[2],
140
+ }
141
+ : { center: [0, 0], zoom: 0 }),
142
+ minZoom: tile_min_zoom,
143
+ maxZoom: is_image_map ? IMAGE_RESOLUTIONS.length - 1 : tile_max_zoom,
144
+ // a tile source that only covers part of the world: keep the
145
+ // view inside what it actually has tiles for
146
+ ...(tile_extent && !is_image_map
147
+ ? { extent: tile_extent, constrainOnlyCenter: true }
148
+ : {}),
90
149
  // image maps live outside the 4326 world: opt out of the
91
150
  // global-projection constraints (they cap resolution at
92
151
  // fit-the-world and clamp the center to [-90, 90]) and use
@@ -119,11 +178,11 @@ export default class OpenLayers extends Map {
119
178
  // Tile Layer — GDPR consent mode defers it until the visitor allows
120
179
  // map tiles (the consent bar renders over the map)
121
180
  const consent = consentManagerOf(this.options);
122
- const tile_service = consentMessage("consent_service_tiles", "map tiles");
181
+ const tile_service = tileService();
123
182
  if (this.options.consent_required && consent) {
124
- if (consent.isGranted(tile_service)) {
183
+ if (consent.isGranted(tile_service.key)) {
125
184
  this._addTileLayer();
126
- } else if (!consent.isDenied(tile_service)) {
185
+ } else if (!consent.isDenied(tile_service.key)) {
127
186
  void this._requestTileConsent(consent, tile_service);
128
187
  }
129
188
  // denied → the map renders with background color and markers only
@@ -131,7 +190,8 @@ export default class OpenLayers extends Map {
131
190
  this._addTileLayer();
132
191
  }
133
192
 
134
- // Create Overall Connection Line
193
+ // Create Overall Connection Line (reserved z-range 10/11 for the
194
+ // route lines; see the stack-order note at _addTileLayer)
135
195
  this._line = this._createLine();
136
196
  this._line.setStyle(this._lineStyle(this.options.line_color_inactive));
137
197
  this._addLineToMap(this._line);
@@ -152,6 +212,7 @@ export default class OpenLayers extends Map {
152
212
 
153
213
  // Stacked raster overlays (base tiles 1.., below the route lines)
154
214
  this._overlay_layers = [];
215
+ this._overlay_entries = [];
155
216
  this._buildOverlays();
156
217
 
157
218
  // Native interactions (pan/zoom), no scroll zoom by default
@@ -174,6 +235,57 @@ export default class OpenLayers extends Map {
174
235
  this._updateAttribution();
175
236
  }
176
237
 
238
+ /* Public accessors
239
+ Consumers get the raw OpenLayers map via `storymap.map`; these hand out
240
+ the internal objects the viewer keeps private (base/overlay/minimap layers,
241
+ the route lines, the markers) so nothing has to be reached for through an
242
+ underscore field. Layer order in `map.getLayers()` is NOT the overlay
243
+ order (creation order is [base, line, line_active, overlay0…] and zIndex is
244
+ a separate axis), so `getOverlayLayers()` is the only correct way to map an
245
+ `overlays[]` index onto a layer.
246
+ ================================================= */
247
+
248
+ /** The base tile layer (the one created for `map_type`), if consent has not deferred it. */
249
+ getBaseLayer(): Layer | null {
250
+ return this._tile_layer ?? null;
251
+ }
252
+
253
+ /** The stacked `overlays[]` layers, in `overlays[]` order. */
254
+ getOverlayLayers(): Layer[] {
255
+ return [...this._overlay_layers];
256
+ }
257
+
258
+ /** One stacked overlay layer by its `overlays[]` index, or `null`. */
259
+ getOverlayLayer(index: number): Layer | null {
260
+ return this._overlay_layers[index] ?? null;
261
+ }
262
+
263
+ /** The minimap's OpenLayers map (the `OverviewMap` control's inner map). */
264
+ getMinimap(): OlMap | null {
265
+ const overview = this._mini_map?.getOverviewMap?.() ?? null;
266
+ return overview ?? null;
267
+ }
268
+
269
+ /** The full (inactive) route line layer. */
270
+ getLine(): Layer | null {
271
+ return this._line ?? null;
272
+ }
273
+
274
+ /** The highlighted route line layer drawn up to the current slide. */
275
+ getLineActive(): Layer | null {
276
+ return this._line_active ?? null;
277
+ }
278
+
279
+ /** The markers, in slide order (the index matches the slide index). */
280
+ getMarkers(): OpenLayersMapMarker[] {
281
+ return [...this._markers];
282
+ }
283
+
284
+ /** One marker by slide index, or `null`. */
285
+ getMarker(index: number): OpenLayersMapMarker | null {
286
+ return this._markers[index] ?? null;
287
+ }
288
+
177
289
  /**
178
290
  * (Re)render the attribution line for the current map type. Called at
179
291
  * creation and on every `map_type` switch, which previously left the
@@ -181,32 +293,90 @@ export default class OpenLayers extends Map {
181
293
  */
182
294
  _updateAttribution(): void {
183
295
  const extras = this._extra_attributions ?? [];
184
- const parts = [this._getAttribution(this.options.map_type), ...extras]
185
- .filter(Boolean)
186
- .join(" | ");
296
+ // Identical credits (e.g. stacked overlays sharing one source, or the
297
+ // attribution option restating the base credit) collapse to one entry
298
+ const parts = [
299
+ ...new Set([...this._getAttribution(this.options.map_type), ...extras].filter(Boolean)),
300
+ ].join(" | ");
187
301
  let el = this._el.map.querySelector(".vco-map-attribution") as HTMLElement | null;
188
302
  if (!el) {
189
303
  this._el.map.insertAdjacentHTML("beforeend", `<div class="vco-map-attribution"></div>`);
190
304
  el = this._el.map.querySelector(".vco-map-attribution") as HTMLElement | null;
191
305
  }
192
306
  if (el) {
193
- el.innerHTML = parts;
307
+ // The library-authored credits are trusted markup, but
308
+ // `options.attribution` and overlay credits come from the
309
+ // storymap JSON. Rather than trusting them, the whole line is
310
+ // re-sanitized: a fragment that is not well-formed markup is
311
+ // unwrapped to text, so an author-supplied credit can never
312
+ // inject an element into the map.
313
+ el.textContent = "";
314
+ el.appendChild(sanitizeSlideText(parts));
194
315
  }
195
316
  }
196
317
 
197
- _getAttribution(map_type: string): string {
318
+ _getAttribution(map_type: string): string[] {
198
319
  const parts = [
199
320
  "<a href='https://storymap.knightlab.com/' target='_blank' class='vco-knightlab-brand'><span>&#x25a0;</span> StoryMapJS</a>",
200
321
  ];
201
- if (map_type === "osm" || map_type === "" || map_type.startsWith("osm")) {
322
+ // Our own `.vco-map-attribution` line is the rendered default (the
323
+ // OL attribution control is not added), so this hand-built line is
324
+ // what the visitor sees. Sources additionally carry the plain-text
325
+ // credit from `_sourceAttributions`, so `source.getAttributions()`
326
+ // and a consumer's own Attribution control are not left empty.
327
+ // Emitting provider credit
328
+ // only for OSM left Stadia, Mapbox and the OL XYZ template providers
329
+ // uncredited, which their terms of service require. The credit is
330
+ // keyed off the resolved map_type, so a custom `{z}` template gets
331
+ // the generic "map data" line and the author can still override it
332
+ // with the `attribution` option.
333
+ if (map_type === "" || map_type.startsWith("osm")) {
202
334
  parts.push(
203
335
  "© <a target='_blank' href='https://www.openstreetmap.org/copyright'>OpenStreetMap</a> contributors",
204
336
  );
337
+ } else if (map_type.startsWith("stadia") || map_type === "stamen") {
338
+ parts.push(
339
+ '© <a target="_blank" rel="noopener noreferrer" href="https://stadiamaps.com/">Stadia Maps</a>, ' +
340
+ '© <a target="_blank" rel="noopener noreferrer" href="https://openmaptiles.org/">OpenMapTiles</a> ' +
341
+ '© <a target="_blank" rel="noopener noreferrer" href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
342
+ );
343
+ } else if (map_type.startsWith("mapbox://")) {
344
+ parts.push('© <a target="_blank" href="https://www.mapbox.com/about/maps/">Mapbox</a>');
345
+ } else if (map_type.startsWith("ch-") || map_type.startsWith("esri")) {
346
+ parts.push('Map data © <a target="_blank" href="https://www.esri.com/">Esri</a>');
347
+ } else {
348
+ parts.push("Map data");
205
349
  }
206
350
  if (this.options.attribution) {
207
351
  parts.push(this.options.attribution);
208
352
  }
209
- return parts.join(" | ");
353
+ return parts;
354
+ }
355
+
356
+ /**
357
+ * The provider credit for `map_type` as plain text (no HTML), for the
358
+ * OpenLayers source `attributions`. Our own `.vco-map-attribution` line
359
+ * stays the rendered default and uses the linked `_getAttribution`
360
+ * version; this makes `source.getAttributions()` (and a consumer's
361
+ * `ol/control/Attribution`) work instead of returning nothing.
362
+ */
363
+ _sourceAttributions(map_type: string): string[] {
364
+ const parts: string[] = [];
365
+ if (map_type === "" || map_type.startsWith("osm")) {
366
+ parts.push("© OpenStreetMap contributors");
367
+ } else if (map_type.startsWith("stadia") || map_type === "stamen") {
368
+ parts.push("© Stadia Maps, © OpenMapTiles © OpenStreetMap contributors");
369
+ } else if (map_type.startsWith("mapbox://")) {
370
+ parts.push("© Mapbox");
371
+ } else if (map_type.startsWith("ch-") || map_type.startsWith("esri")) {
372
+ parts.push("Map data © Esri");
373
+ } else {
374
+ parts.push("Map data");
375
+ }
376
+ if (this.options.attribution) {
377
+ parts.push(this.options.attribution);
378
+ }
379
+ return parts;
210
380
  }
211
381
 
212
382
  /* Create Tile Layer
@@ -227,7 +397,7 @@ export default class OpenLayers extends Map {
227
397
  /**
228
398
  * Ask for tile consent, then attach the layers if allowed.
229
399
  */
230
- async _requestTileConsent(consent: ConsentManager, tile_service: string) {
400
+ async _requestTileConsent(consent: ConsentManager, tile_service: ConsentService) {
231
401
  if (await consent.request(tile_service, "", this._el.map)) {
232
402
  this._onTilesAllowed();
233
403
  }
@@ -240,24 +410,36 @@ export default class OpenLayers extends Map {
240
410
  * Overlays sit above the base tiles (z 1..n) and below the route lines
241
411
  * (z 10/11); every entry accepts any `map_type` value plus declarative
242
412
  * presentation, so hosts no longer capture and patch layer objects.
413
+ * An entry carrying a `georeference` places a IIIF image on the
414
+ * geographic map instead of stacking a tile source.
243
415
  */
244
416
  _buildOverlays(): void {
245
417
  for (const layer of this._overlay_layers) {
246
418
  this._map.removeLayer(layer);
247
419
  }
248
420
  this._overlay_layers = [];
249
- const consent = consentManagerOf(this.options);
250
- const tile_service = consentMessage("consent_service_tiles", "map tiles");
251
- if (
252
- this.options.consent_required &&
253
- consent &&
254
- !consent.isGranted(tile_service)
255
- ) {
421
+ this._overlay_entries = [];
422
+ if (!this._tilesAllowed()) {
256
423
  return;
257
424
  }
258
425
  const overlays = this.options.overlays ?? [];
259
426
  overlays.forEach((entry, i) => {
260
- const layer = this._createTileLayer(entry.map_type);
427
+ let layer: Layer | null = null;
428
+ if (entry.georeference) {
429
+ layer = this._createGeoreferencedOverlay(entry);
430
+ } else if (entry.map_type) {
431
+ layer = this._createTileLayer(entry.map_type);
432
+ } else {
433
+ console.warn(
434
+ "StoryMapJS: an overlays[] entry has neither map_type nor georeference and was skipped:",
435
+ entry,
436
+ );
437
+ }
438
+ if (!layer) {
439
+ return;
440
+ }
441
+ // reserved z-range 1..n for stacked overlays (base tiles 0,
442
+ // route lines 10/11; see _addTileLayer)
261
443
  layer.setZIndex(1 + i);
262
444
  if (entry.opacity !== undefined) {
263
445
  layer.setOpacity(entry.opacity);
@@ -280,11 +462,78 @@ export default class OpenLayers extends Map {
280
462
  }
281
463
  this._map.addLayer(layer);
282
464
  this._overlay_layers.push(layer);
465
+ this._overlay_entries.push(entry);
283
466
  this._paintOverlayBlend(entry);
284
467
  });
285
468
  this._syncOverlayAttributions();
286
469
  }
287
470
 
471
+ /**
472
+ * Build a stacked overlay from a IIIF Georeference Extension payload
473
+ * (`overlays[].georeference`): the ground control points are fitted
474
+ * affinely and the image is placed as a geographic extent, which
475
+ * OpenLayers reprojects onto the view exactly like the IIIF basemap.
476
+ *
477
+ * The layer is returned empty and filled in asynchronously, mirroring
478
+ * the `iiif` basemap path — only the fit (pure, synchronous) decides
479
+ * whether a layer exists at all, so overlay indices stay stable.
480
+ */
481
+ _createGeoreferencedOverlay(entry: StorymapOverlayLayer): TileLayer | null {
482
+ const georeference = entry.georeference;
483
+ if (!georeference) {
484
+ return null;
485
+ }
486
+ // image-space views (iiif/zoomify) have no geographic space to
487
+ // place the sheet in
488
+ if (this._map.getView().getProjection().getCode() !== "EPSG:3857") {
489
+ console.warn(
490
+ "StoryMapJS: a georeferenced overlay was skipped because this map has no geographic view (image maps cannot place sheets).",
491
+ );
492
+ return null;
493
+ }
494
+ const fit = fitGeoreference(georeference.body, georeference.width, georeference.height);
495
+ if (fit.kind === "skipped") {
496
+ console.warn(
497
+ `StoryMapJS: georeferenced overlay ${georeference.url} was skipped: ${fit.reason}.`,
498
+ );
499
+ return null;
500
+ }
501
+ const layer = new TileLayer();
502
+ const info_url = resolveInfoJsonUrl(georeference.url);
503
+ fetch(info_url)
504
+ .then((r) => r.json())
505
+ .then((info: unknown) => {
506
+ const parsed = new IIIFInfo(
507
+ info as ImageInformationResponse,
508
+ ).getTileSourceOptions();
509
+ const size = info as { width?: number; height?: number };
510
+ if (typeof size.width !== "number" || typeof size.height !== "number") {
511
+ console.error("IIIF info.json is missing width/height:", info_url);
512
+ layer.setVisible(false);
513
+ return;
514
+ }
515
+ const geo_source = new IIIF({
516
+ ...(parsed ?? {}),
517
+ projection: "EPSG:4326",
518
+ // the fitted corner box: the affine placement of the
519
+ // image in geographic coordinates
520
+ extent: fit.bbox,
521
+ size: [size.width, size.height],
522
+ crossOrigin: "anonymous",
523
+ attributions: entry.attribution ? [entry.attribution] : [],
524
+ });
525
+ layer.setSource(geo_source);
526
+ this._fireImageready(geo_source, "iiif", layer);
527
+ })
528
+ .catch((err) => {
529
+ // the layer stays in place (indices must not shift) but
530
+ // renders nothing; the console carries the reason
531
+ console.error("IIIF info.json could not be loaded:", info_url, err?.stack || err);
532
+ layer.setVisible(false);
533
+ });
534
+ return layer;
535
+ }
536
+
288
537
  /**
289
538
  * Convert a lon/lat clip box to view units. Only meaningful on
290
539
  * mercator maps; image-space maps (iiif/zoomify) and malformed
@@ -310,10 +559,7 @@ export default class OpenLayers extends Map {
310
559
  * frames (bounded: rendering settles within a frame or two of any
311
560
  * add/visibility change).
312
561
  */
313
- _paintOverlayBlend(
314
- entry: { className?: string; blendMode?: string },
315
- retries = 60,
316
- ): void {
562
+ _paintOverlayBlend(entry: { className?: string; blendMode?: string }, retries = 60): void {
317
563
  if (!entry.blendMode || !entry.className) {
318
564
  return;
319
565
  }
@@ -337,10 +583,9 @@ export default class OpenLayers extends Map {
337
583
 
338
584
  /** Refresh the attribution line with the visible overlays' credits. */
339
585
  _syncOverlayAttributions(): void {
340
- const overlays = this.options.overlays ?? [];
341
586
  const parts: string[] = [];
342
587
  this._overlay_layers.forEach((layer, i) => {
343
- const credit = overlays[i]?.attribution;
588
+ const credit = this._overlay_entries[i]?.attribution;
344
589
  if (credit && layer.getVisible()) {
345
590
  parts.push(credit);
346
591
  }
@@ -348,19 +593,29 @@ export default class OpenLayers extends Map {
348
593
  this.setExtraAttributions(parts);
349
594
  }
350
595
 
351
- /** Number of stacked overlay layers (see the `overlays` option). */
596
+ /**
597
+ * Number of stacked overlay layers (see the `overlays` option). Alias:
598
+ * `getOverlayLayers().length`.
599
+ */
352
600
  getOverlayCount(): number {
353
601
  return this._overlay_layers.length;
354
602
  }
355
603
 
356
- /** Show or hide a stacked overlay by index (re-syncs attribution). */
604
+ /**
605
+ * Show or hide a stacked overlay by index (re-syncs attribution). The
606
+ * index counts built layers, i.e. it skips malformed entries.
607
+ *
608
+ * Prefer `getOverlayLayer(index).setVisible(v)` on the layer itself; this
609
+ * wrapper exists because it also re-syncs the attribution line and the
610
+ * overlay blend mode, which a bare `setVisible()` does not.
611
+ */
357
612
  setOverlayVisible(index: number, visible: boolean): void {
358
613
  const layer = this._overlay_layers[index];
359
614
  if (!layer) {
360
615
  return;
361
616
  }
362
617
  layer.setVisible(visible);
363
- const entry = (this.options.overlays ?? [])[index];
618
+ const entry = this._overlay_entries[index];
364
619
  if (entry && visible) {
365
620
  this._paintOverlayBlend(entry);
366
621
  }
@@ -368,6 +623,9 @@ export default class OpenLayers extends Map {
368
623
  }
369
624
 
370
625
  /** Set a stacked overlay's opacity by index. */
626
+ /**
627
+ * Alias: `getOverlayLayer(index)?.setOpacity(opacity)`.
628
+ */
371
629
  setOverlayOpacity(index: number, opacity: number): void {
372
630
  this._overlay_layers[index]?.setOpacity(opacity);
373
631
  }
@@ -392,8 +650,11 @@ export default class OpenLayers extends Map {
392
650
  const marker = this._markers[this.current_marker];
393
651
  if (marker.data.type === "overview") {
394
652
  this._markerOverview();
395
- } else if (this._hasLocation(marker.data)) {
396
- this._fitView(this._map, [[marker.data.location.lon, marker.data.location.lat]], 0);
653
+ } else {
654
+ const latlon = marker.latLon();
655
+ if (latlon) {
656
+ this._fitView(this._map, [[latlon.lon, latlon.lat]], 0);
657
+ }
397
658
  }
398
659
  }
399
660
  }
@@ -477,7 +738,206 @@ export default class OpenLayers extends Map {
477
738
  return { zoom, center };
478
739
  }
479
740
 
480
- _createTileLayer(map_type: string): TileLayer {
741
+ _createTileLayer(map_type: string): Layer {
742
+ // issue #473: custom OpenLayers tile layer/source factory first —
743
+ // the base layer, overlays, minimap and runtime map_type switches
744
+ // all funnel through here, so one check covers them
745
+ const factory = this.options.tile_source_factory;
746
+ if (typeof factory === "function") {
747
+ const custom = factory(map_type, {
748
+ options: this.options,
749
+ createDefault: () => this._createDefaultTileLayer(map_type),
750
+ });
751
+ if (custom) {
752
+ // any ol/layer/Layer passes through as-is — including
753
+ // subclasses without a getSource (e.g. an Allmaps
754
+ // WarpedMapLayer), which must not be wrapped; a bare Source is
755
+ // wrapped in a TileLayer
756
+ if (
757
+ custom instanceof Layer ||
758
+ typeof (custom as { getSource?: unknown }).getSource === "function"
759
+ ) {
760
+ return custom as Layer;
761
+ }
762
+ return new TileLayer({ source: custom as unknown as XYZ });
763
+ }
764
+ }
765
+ return this._createDefaultTileLayer(map_type);
766
+ }
767
+
768
+ /**
769
+ * A layer's source, or null when it has none. Custom `Layer` subclasses
770
+ * passed through `_createTileLayer` (e.g. Allmaps' `WarpedMapLayer`)
771
+ * deliberately have no `getSource`, so every read goes through here
772
+ * instead of calling the method directly.
773
+ */
774
+ _sourceOf(layer: Layer | null | undefined): Source | null {
775
+ if (!layer || typeof (layer as { getSource?: unknown }).getSource !== "function") {
776
+ return null;
777
+ }
778
+ return (layer as TileLayer).getSource();
779
+ }
780
+
781
+ /**
782
+ * Move the map into another element, keeping OpenLayers in step.
783
+ *
784
+ * `DomMixed.addTo()` (the original semantics, unchanged: append the map
785
+ * container, fire `added`) only moves DOM — the `ol/Map` still has the
786
+ * viewport it measured at construction, so it would keep painting at the
787
+ * old size. Re-attach the target, which `removeFrom()` detached, and
788
+ * re-measure. The argument must be the new parent.
789
+ */
790
+ addTo(container: HTMLElement): this {
791
+ super.addTo(container);
792
+ this._map.setTarget(this._el.map);
793
+ this._map.updateSize();
794
+ return this;
795
+ }
796
+
797
+ /**
798
+ * Move the map out of `container`. Untargets the OpenLayers map so it
799
+ * does not render into a detached node; a later {@link addTo} re-attaches
800
+ * it. The argument must be the current parent.
801
+ */
802
+ removeFrom(container: HTMLElement): this {
803
+ super.removeFrom(container);
804
+ this._map.setTarget(undefined);
805
+ return this;
806
+ }
807
+
808
+ /**
809
+ * True when the map is a plain image in an `EPSG:4326` "image space"
810
+ * rather than a geographic map: a IIIF image presented as a picture of
811
+ * the world (`map_as_image`), not georeferenced.
812
+ *
813
+ * In this mode the view projection is `EPSG:4326` with degrees as
814
+ * resolution and the view is fitted to the image extent, so
815
+ * `getCenter()`/`getZoom()` are in degrees/pixels-per-degree — not
816
+ * mercator metres. Marker locations are pixel offsets into the image,
817
+ * not lat/lon. Georeferenced IIIF (`map_bbox` with `map_as_image`
818
+ * omitted) is a normal mercator map; use `isImageSpace()` rather than
819
+ * testing `map_type` to tell the two apart.
820
+ */
821
+ isImageSpace(): boolean {
822
+ return this.options.map_type === "iiif" && this.options.map_as_image === true;
823
+ }
824
+
825
+ /**
826
+ * Stop the line animation, drop the minimap control and dispose the
827
+ * OpenLayers map. Called by `StoryMap.dispose()`; the engine must not be
828
+ * used afterwards.
829
+ */
830
+ dispose(): void {
831
+ if (this._line_animation !== null) {
832
+ cancelAnimationFrame(this._line_animation);
833
+ this._line_animation = null;
834
+ }
835
+ // markers own their click listener and an overlay; disposing the
836
+ // collection releases both
837
+ for (const marker of this._markers) {
838
+ marker.dispose?.();
839
+ }
840
+ this._markers = [];
841
+ // detach the viewport first: dispose() alone leaves the canvas in
842
+ // the caller's DOM
843
+ this._map.setTarget(undefined);
844
+ this._map.dispose();
845
+ }
846
+
847
+ /**
848
+ * Fire `imageready` once `source` is usable (and immediately when it
849
+ * already is). Image sources — IIIF, zoomify, the deferred tile layer —
850
+ * attach asynchronously, so this is the only outward signal that the
851
+ * imagery is really on the map; the `loaded` event can fire before the
852
+ * source is attached at all.
853
+ */
854
+ _fireImageready(source: { getState?(): string }, kind: ImagereadyKind, layer?: Layer): void {
855
+ // one event per source: the base layer and the minimap can be asked
856
+ // about the same source, and a host toggling layers should not have
857
+ // to filter duplicates
858
+ if (this._imageready_fired.has(source)) return;
859
+ this._imageready_fired.add(source);
860
+ const payload = { source, kind, layer: layer ?? null };
861
+ if (typeof source.getState !== "function" || source.getState() === "ready") {
862
+ this.fire("imageready", payload);
863
+ return;
864
+ }
865
+ const onChange = () => {
866
+ if (source.getState?.() === "ready") {
867
+ (source as { un?: (t: string, f: () => void) => void }).un?.("change", onChange);
868
+ this.fire("imageready", payload);
869
+ }
870
+ };
871
+ (source as { once?: (t: string, f: () => void) => void }).once?.("change", onChange);
872
+ }
873
+
874
+ /** Computed zooms take precedence over the authored slide zoom. */
875
+ _markerZoom(index: number): number | undefined {
876
+ return this._marker_zooms[index] ?? super._markerZoom(index);
877
+ }
878
+
879
+ /**
880
+ * The TileJSON metadata, but only when it is describing `url`.
881
+ *
882
+ * `tilejson` describes the map's tile source, so its `tiles` template is
883
+ * the one the map_type resolved to. When it names a different template it
884
+ * is not about this source, and neither the zoom ladder nor the initial
885
+ * view should be taken from it.
886
+ */
887
+ _tilejsonFor(url: string): StorymapTilejson | undefined {
888
+ const tilejson = this.options.tilejson;
889
+ if (tilejson === undefined) return undefined;
890
+ const tiles = Array.isArray(tilejson.tiles) ? tilejson.tiles[0] : tilejson.tiles;
891
+ return tiles === url ? tilejson : undefined;
892
+ }
893
+
894
+ /**
895
+ * An XYZ source for a URL template, honouring the TileJSON metadata of the
896
+ * map's own tile source if it is describing this template (§2.9).
897
+ *
898
+ * `minzoom`/`maxzoom` become the tile grid's zoom range, so OpenLayers asks
899
+ * for levels the service actually has and stops past the ones it does not.
900
+ * `scheme: "tms"` is the TMS row order, which is the same URL with the tile
901
+ * row counted from the bottom.
902
+ */
903
+ _xyzSource(url: string): XYZ {
904
+ const tilejson = this._tilejsonFor(url);
905
+ const options: Record<string, unknown> = {
906
+ url,
907
+ attributions: this._sourceAttributions(url),
908
+ crossOrigin: "anonymous",
909
+ };
910
+ if (tilejson !== undefined) {
911
+ if (tilejson.minzoom !== undefined || tilejson.maxzoom !== undefined) {
912
+ options.minZoom = tilejson.minzoom ?? 0;
913
+ options.maxZoom = tilejson.maxzoom ?? MAX_ZOOM;
914
+ }
915
+ }
916
+ const source = new XYZ(options as never);
917
+ if (tilejson?.scheme === "tms") {
918
+ // TMS numbers tile rows from the bottom of the pyramid; XYZ counts
919
+ // them from the top, so the row is flipped against the source's
920
+ // deepest zoom.
921
+ //
922
+ // This is assigned rather than passed as the `tileUrlFunction`
923
+ // constructor option, which this version of ol/source/XYZ does not
924
+ // read.
925
+ const deepest = tilejson.maxzoom ?? MAX_ZOOM;
926
+ source.tileUrlFunction = (tileCoord: number[] | undefined): string | undefined => {
927
+ if (tileCoord === undefined) return undefined;
928
+ // OpenLayers hands over [z, x, y]
929
+ const [z, x, y] = tileCoord;
930
+ return url
931
+ .replace("{z}", String(z))
932
+ .replace("{x}", String(x))
933
+ .replace("{y}", String(2 ** deepest - 1 - y))
934
+ .replace("{ratio}", "");
935
+ };
936
+ }
937
+ return source;
938
+ }
939
+
940
+ _createDefaultTileLayer(map_type: string): TileLayer {
481
941
  const _map_type_arr = map_type.split(":");
482
942
 
483
943
  switch (_map_type_arr[0]) {
@@ -494,7 +954,7 @@ export default class OpenLayers extends Map {
494
954
  return new TileLayer({
495
955
  source: new XYZ({
496
956
  url: mapbox_url,
497
- attributions: [],
957
+ attributions: this._sourceAttributions(map_type),
498
958
  crossOrigin: "anonymous",
499
959
  }),
500
960
  });
@@ -502,11 +962,18 @@ export default class OpenLayers extends Map {
502
962
  console.error(
503
963
  "StoryMapJS: legacy 'mapbox:<style>' map types are no longer supported (the Mapbox v4 tile API was retired); use 'mapbox://styles/<user>/<style>' with map_access_token instead.",
504
964
  );
505
- return new TileLayer({ source: new OSM({ attributions: [] }) });
965
+ return new TileLayer({
966
+ source: new OSM({ attributions: this._sourceAttributions(map_type) }),
967
+ });
506
968
  }
507
969
 
508
970
  case "stadia": {
509
- let style_url = "osm:standard";
971
+ // Stadia hosts a style *set*, not a single style. A bare
972
+ // `map_type: "stadia"` used to fall back to the literal
973
+ // "osm:standard", which is not one of them — every tile 404'd.
974
+ // `alidade_smooth` is the correct default here (matches the
975
+ // remapped `stamen` case below).
976
+ let style_url = "alidade_smooth";
510
977
  if (_map_type_arr.length > 1) {
511
978
  style_url = _map_type_arr.slice(1).join(":");
512
979
  if (this.options.map_access_token) {
@@ -515,8 +982,8 @@ export default class OpenLayers extends Map {
515
982
  }
516
983
  return new TileLayer({
517
984
  source: new XYZ({
518
- url: `https://tiles.stadiamaps.com/tiles/${style_url}/{z}/{x}/{y}.png`,
519
- attributions: [],
985
+ url: `https://tiles.stadiamaps.com/tiles/${style_url}/{z}/{x}/{y}{r}.png`,
986
+ attributions: this._sourceAttributions(map_type),
520
987
  }),
521
988
  });
522
989
  }
@@ -526,7 +993,7 @@ export default class OpenLayers extends Map {
526
993
  return new TileLayer({
527
994
  source: new XYZ({
528
995
  url: "https://tiles.stadiamaps.com/tiles/stamen_toner_lite/{z}/{x}/{y}.png",
529
- attributions: [],
996
+ attributions: this._sourceAttributions(map_type),
530
997
  }),
531
998
  });
532
999
 
@@ -560,6 +1027,7 @@ export default class OpenLayers extends Map {
560
1027
  attributions: this.options.iiif.attribution || [],
561
1028
  });
562
1029
  iiif_layer.setSource(source);
1030
+ this._fireImageready(source, "iiif", iiif_layer);
563
1031
  if (source.getState() === "ready") {
564
1032
  this._markerOverview();
565
1033
  } else {
@@ -587,15 +1055,13 @@ export default class OpenLayers extends Map {
587
1055
  if (!map_type.includes("{z}")) {
588
1056
  return this._createVectorStyleLayer(map_type);
589
1057
  }
590
- return new TileLayer({
591
- source: new XYZ({ url: map_type, attributions: [], crossOrigin: "anonymous" }),
592
- });
1058
+ return new TileLayer({ source: this._xyzSource(map_type) });
593
1059
 
594
1060
  case "ch-watercolor":
595
1061
  return new TileLayer({
596
1062
  source: new XYZ({
597
1063
  url: "https://watercolormaps.collection.cooperhewitt.org/tile/watercolor/{z}/{x}/{y}.jpg",
598
- attributions: [],
1064
+ attributions: this._sourceAttributions(map_type),
599
1065
  maxZoom: 16,
600
1066
  }),
601
1067
  });
@@ -611,50 +1077,56 @@ export default class OpenLayers extends Map {
611
1077
  console.error(
612
1078
  "StoryMapJS: map_type 'zoomify' needs a zoomify image pyramid (path, width, height) in the storymap data.",
613
1079
  );
614
- return new TileLayer({ source: new OSM({ attributions: [] }) });
1080
+ return new TileLayer({
1081
+ source: new OSM({ attributions: this._sourceAttributions(map_type) }),
1082
+ });
615
1083
  }
616
1084
  const path = (this.options.zoomify as { path?: string }).path ?? "";
617
1085
  const { sizes, maxZoom: pyramidMaxZoom } = pyramid;
618
1086
  const gridX = (z: number) => Math.ceil(sizes[z][0] / 256);
619
1087
  const gridY = (z: number) => Math.ceil(sizes[z][1] / 256);
620
1088
 
621
- return new TileLayer({
622
- source: new XYZ({
623
- tileGrid: pyramid.tileGrid,
624
- crossOrigin: "anonymous",
625
- attributions: [],
626
- tileUrlFunction: (tile: number[]) => {
627
- const [tileZ, x, y] = tile;
628
- // the ladder is shifted one level down: mercator
629
- // zoom z serves the pyramid level max(0, z - 1)
630
- const z = Math.max(0, tileZ - 1);
631
- if (z > pyramidMaxZoom) return undefined;
632
- if (x < 0 || x >= gridX(z) || y < 0 || y >= gridY(z)) {
633
- return undefined;
634
- }
635
- // TileGroup index: the running tile number ÷ 256
636
- let num = 0;
637
- for (let zz = 0; zz < z; zz++) {
638
- num += gridX(zz) * gridY(zz);
639
- }
640
- num += y * gridX(z) + x;
641
- return `${path}TileGroup${Math.floor(num / 256)}/${z}-${x}-${y}.jpg`;
642
- },
643
- // Zoomify edge tiles are cropped to the image bounds
644
- // (e.g. a 256x19 bottom strip); OpenLayers draws the
645
- // loaded image over the whole 256x256 tile box, which
646
- // stretched those strips across the cell (the smeared
647
- // bottom in the Bosch overview, stretched right/bottom
648
- // edges in the Literary Trail). Pad them onto a full
649
- // tile canvas instead.
650
- tileLoadFunction: (tile, src) => {
651
- const imageTile = tile as ImageTile;
652
- const image = imageTile.getImage() as HTMLImageElement;
653
- image.onload = () => padCroppedZoomifyTile(imageTile, image);
654
- image.src = src;
655
- },
656
- }),
1089
+ const zoomify_source = new XYZ({
1090
+ tileGrid: pyramid.tileGrid,
1091
+ crossOrigin: "anonymous",
1092
+ attributions: this._sourceAttributions(map_type),
1093
+ tileUrlFunction: (tile: number[]) => {
1094
+ const [tileZ, x, y] = tile;
1095
+ // the ladder is shifted one level down: mercator
1096
+ // zoom z serves the pyramid level max(0, z - 1)
1097
+ const z = Math.max(0, tileZ - 1);
1098
+ if (z > pyramidMaxZoom) return undefined;
1099
+ if (x < 0 || x >= gridX(z) || y < 0 || y >= gridY(z)) {
1100
+ return undefined;
1101
+ }
1102
+ // TileGroup index: the running tile number ÷ 256
1103
+ let num = 0;
1104
+ for (let zz = 0; zz < z; zz++) {
1105
+ num += gridX(zz) * gridY(zz);
1106
+ }
1107
+ num += y * gridX(z) + x;
1108
+ return `${path}TileGroup${Math.floor(num / 256)}/${z}-${x}-${y}.jpg`;
1109
+ },
1110
+ // Zoomify edge tiles are cropped to the image bounds
1111
+ // (e.g. a 256x19 bottom strip); OpenLayers draws the
1112
+ // loaded image over the whole 256x256 tile box, which
1113
+ // stretched those strips across the cell (the smeared
1114
+ // bottom in the Bosch overview, stretched right/bottom
1115
+ // edges in the Literary Trail). Pad them onto a full
1116
+ // tile canvas instead.
1117
+ tileLoadFunction: (tile, src) => {
1118
+ const imageTile = tile as ImageTile;
1119
+ const image = imageTile.getImage() as HTMLImageElement;
1120
+ image.onload = () => padCroppedZoomifyTile(imageTile, image);
1121
+ image.src = src;
1122
+ },
657
1123
  });
1124
+ const zoomify_layer = new TileLayer({ source: zoomify_source });
1125
+ // the zoomify base never announced itself, so a host waiting
1126
+ // on imageready hung on exactly the legacy stories that need
1127
+ // it most; the per-source dedup keeps this to one event
1128
+ this._fireImageready(zoomify_source, "zoomify", zoomify_layer);
1129
+ return zoomify_layer;
658
1130
  }
659
1131
 
660
1132
  case "osm": {
@@ -667,7 +1139,9 @@ export default class OpenLayers extends Map {
667
1139
  `https://tiles.openfreemap.org/styles/${style_name}`,
668
1140
  );
669
1141
  }
670
- return new TileLayer({ source: new OSM({ attributions: [] }) });
1142
+ return new TileLayer({
1143
+ source: new OSM({ attributions: this._sourceAttributions(map_type) }),
1144
+ });
671
1145
  }
672
1146
  default: {
673
1147
  // Relative/custom templates (./tiles/{z}/{x}/{y}.png,
@@ -678,18 +1152,14 @@ export default class OpenLayers extends Map {
678
1152
  // {z} render as vector styles, anything else falls back to
679
1153
  // OSM.
680
1154
  if (map_type.includes("{z}")) {
681
- return new TileLayer({
682
- source: new XYZ({
683
- url: map_type,
684
- attributions: [],
685
- crossOrigin: "anonymous",
686
- }),
687
- });
1155
+ return new TileLayer({ source: this._xyzSource(map_type) });
688
1156
  }
689
1157
  if (map_type.includes("/") || map_type.endsWith(".json")) {
690
1158
  return this._createVectorStyleLayer(map_type);
691
1159
  }
692
- return new TileLayer({ source: new OSM({ attributions: [] }) });
1160
+ return new TileLayer({
1161
+ source: new OSM({ attributions: this._sourceAttributions(map_type) }),
1162
+ });
693
1163
  }
694
1164
  }
695
1165
  }
@@ -718,15 +1188,10 @@ export default class OpenLayers extends Map {
718
1188
  // consent mode: the minimap layer is only created once map tiles are
719
1189
  // allowed — creating an IIIF/vector layer object fetches info.json or
720
1190
  // the style JSON immediately, which must not happen while denied
721
- const consent = consentManagerOf(this.options);
722
- const tile_service = consentMessage("consent_service_tiles", "map tiles");
723
- const tiles_allowed = !(
724
- this.options.consent_required &&
725
- consent &&
726
- !consent.isGranted(tile_service)
727
- );
728
- this._tile_layer_mini = tiles_allowed ? this._createTileLayer(this.options.map_type) : null;
729
- const is_image_map = this.options.map_type === "iiif" && this.options.map_as_image;
1191
+ this._tile_layer_mini = this._tilesAllowed()
1192
+ ? this._createTileLayer(this.options.map_type)
1193
+ : null;
1194
+ const is_image_map = this.isImageSpace();
730
1195
  // Legacy zoomify maps are mercator-based: the minimap fits the image's
731
1196
  // mercator bounds with a free zoom so the whole image stays visible
732
1197
  // at a downscaled (sharp) pyramid level
@@ -828,9 +1293,7 @@ export default class OpenLayers extends Map {
828
1293
  // the minimap starts collapsed (no layout size yet) in its
829
1294
  // 150x100 box — fall back to that until it expands
830
1295
  const size =
831
- raw_size && raw_size[0] >= 50 && raw_size[1] >= 50
832
- ? raw_size
833
- : [150, 100];
1296
+ raw_size && raw_size[0] >= 50 && raw_size[1] >= 50 ? raw_size : [150, 100];
834
1297
  overview_map.getView().fit(zoomify_pyramid.extent, {
835
1298
  size: size,
836
1299
  });
@@ -842,10 +1305,15 @@ export default class OpenLayers extends Map {
842
1305
  } else if (!is_zoomify && this.bounds_array && this.bounds_array.length) {
843
1306
  // the minimap shows the story's world: with a bbox set, markers
844
1307
  // outside of the box are unreachable and must not skew the fit
845
- this._fitView(this._mini_map.getOverviewMap(), this.bounds_array, 0, this._bboxExtent() !== null);
1308
+ this._fitView(
1309
+ this._mini_map.getOverviewMap(),
1310
+ this.bounds_array,
1311
+ 0,
1312
+ this._bboxExtent() !== null,
1313
+ );
846
1314
  }
847
1315
 
848
- if (this.options.map_type === "iiif" && this.options.map_as_image) {
1316
+ if (this.isImageSpace()) {
849
1317
  // in image mode there are no geo markers to fit, so show the
850
1318
  // whole image instead (issues #465, #355)
851
1319
  this._fitMiniMapToImage();
@@ -856,7 +1324,7 @@ export default class OpenLayers extends Map {
856
1324
  if (!this._tile_layer_mini) return;
857
1325
  const fit_mini_image = () => {
858
1326
  try {
859
- const mini_source = this._tile_layer_mini?.getSource() as {
1327
+ const mini_source = this._sourceOf(this._tile_layer_mini) as {
860
1328
  getTileGrid?: () => { getExtent(): number[] };
861
1329
  } | null;
862
1330
  const grid = mini_source?.getTileGrid?.();
@@ -874,8 +1342,17 @@ export default class OpenLayers extends Map {
874
1342
  console.warn("IIIF minimap fit failed:", e);
875
1343
  }
876
1344
  };
877
- const mini_source = this._tile_layer_mini.getSource();
1345
+ const mini_source = this._sourceOf(this._tile_layer_mini);
878
1346
  if (mini_source) {
1347
+ this._fireImageready(
1348
+ mini_source,
1349
+ this.options.map_type === "zoomify"
1350
+ ? "zoomify"
1351
+ : this.options.map_type === "iiif"
1352
+ ? "iiif"
1353
+ : "tiles",
1354
+ this._tile_layer_mini,
1355
+ );
879
1356
  if (mini_source.getState() === "ready") {
880
1357
  fit_mini_image();
881
1358
  } else {
@@ -886,7 +1363,7 @@ export default class OpenLayers extends Map {
886
1363
  } else {
887
1364
  // the mini layer sets its source asynchronously
888
1365
  this._tile_layer_mini.once("change:source", () => {
889
- const src = this._tile_layer_mini?.getSource();
1366
+ const src = this._sourceOf(this._tile_layer_mini);
890
1367
  if (!src) return;
891
1368
  if (src.getState() === "ready") {
892
1369
  fit_mini_image();
@@ -923,12 +1400,16 @@ export default class OpenLayers extends Map {
923
1400
  if (this._map.getView().getProjection().getCode() === "EPSG:4326") {
924
1401
  return;
925
1402
  }
926
- const real = this._markers.filter(
927
- (m) => m.data.real_marker && m.data.location?.lon !== undefined,
928
- );
929
- const unwrapped = this._unwrapLongitudes(real.map((m) => m.data.location.lon as number));
930
- real.forEach((m, i) => {
931
- m._overlay?.setPosition(fromLonLat([unwrapped[i], m.data.location.lat as number]));
1403
+ const real: { marker: OpenLayersMapMarker; latlon: LatLngLiteral }[] = [];
1404
+ for (const marker of this._markers) {
1405
+ const latlon = marker.latLon();
1406
+ if (latlon) {
1407
+ real.push({ marker: marker, latlon: latlon });
1408
+ }
1409
+ }
1410
+ const unwrapped = this._unwrapLongitudes(real.map((entry) => entry.latlon.lon));
1411
+ real.forEach((entry, i) => {
1412
+ entry.marker._overlay?.setPosition(fromLonLat([unwrapped[i], entry.latlon.lat]));
932
1413
  });
933
1414
  }
934
1415
 
@@ -941,18 +1422,16 @@ export default class OpenLayers extends Map {
941
1422
  /* Marker helpers
942
1423
  ================================================== */
943
1424
  _getAllMarkersBounds(markers_array: OpenLayersMapMarker[]): number[][] {
944
- const coords = [];
945
- for (let i = 0; i < markers_array.length; i++) {
946
- if (markers_array[i].data.real_marker) {
947
- coords.push([
948
- markers_array[i].data.location.lon,
949
- markers_array[i].data.location.lat,
950
- ]);
1425
+ const coords: number[][] = [];
1426
+ for (const marker of markers_array) {
1427
+ const latlon = marker.latLon();
1428
+ if (latlon) {
1429
+ coords.push([latlon.lon, latlon.lat]);
951
1430
  }
952
1431
  }
953
1432
  // unwrap dateline crossings so fits don't span the whole globe
954
1433
  // (issue #381)
955
- const lons = this._unwrapLongitudes(coords.map((c) => c[0] as number));
1434
+ const lons = this._unwrapLongitudes(coords.map((c) => c[0]));
956
1435
  return coords.map((c, i) => [lons[i], c[1]]);
957
1436
  }
958
1437
 
@@ -986,8 +1465,7 @@ export default class OpenLayers extends Map {
986
1465
  _bboxExtent(): number[] | null {
987
1466
  const bbox = this.options.map_bbox as number[] | null | undefined;
988
1467
  if (!bbox || bbox.length !== 4) return null;
989
- const is_image_space = this.options.map_type === "iiif" && this.options.map_as_image;
990
- if (is_image_space) return bbox;
1468
+ if (this.isImageSpace()) return bbox;
991
1469
  // NB: fromLonLat transforms a single [lon, lat] pair — transform the
992
1470
  // two corners separately
993
1471
  const min = fromLonLat([bbox[0], bbox[1]]);
@@ -1004,7 +1482,17 @@ export default class OpenLayers extends Map {
1004
1482
  _opaquePanelPadding(): [number, number, number, number] {
1005
1483
  const padding: [number, number, number, number] = [15, 15, 15, 15];
1006
1484
  if (typeof document === "undefined") return padding;
1007
- const panel = document.querySelector(".vco-storyslider .vco-slide.vco-active .vco-text");
1485
+ // Scope the search to THIS viewer's root. The slide panel is a sibling
1486
+ // of the map, not a descendant, so it is not reachable from
1487
+ // this._el.container (the .vco-map div); a document-wide query would
1488
+ // find whichever panel comes first on the page, and then measure this
1489
+ // map against another viewer's rectangle — the overlap test below then
1490
+ // decides padding for a panel that may not overlap this map at all.
1491
+ const root = this._el.container.closest?.(".vco-storymap");
1492
+ // no viewer root means the map is standalone, so there is no slide
1493
+ // panel to make room for
1494
+ if (!root) return padding;
1495
+ const panel = root.querySelector(".vco-storyslider .vco-slide.vco-active .vco-text");
1008
1496
  if (!panel) return padding;
1009
1497
  const bg = getComputedStyle(panel).backgroundColor;
1010
1498
  const match = /rgba?\(([^)]+)\)/.exec(bg);
@@ -1062,52 +1550,60 @@ export default class OpenLayers extends Map {
1062
1550
  padding: this._opaquePanelPadding(),
1063
1551
  maxZoom: 12,
1064
1552
  duration: duration,
1065
- easing: this.options.ease as ((t: number) => number) | undefined,
1553
+ easing: this._easing,
1066
1554
  });
1067
1555
  }
1068
1556
 
1069
1557
  _calculateMarkerZooms(): void {
1070
- for (let i = 0; i < this._markers.length; i++) {
1071
- if (this._markers[i].data.location) {
1072
- const marker = this._markers[i];
1073
- let marker_location, calculated_zoom;
1074
-
1075
- // MARKER LOCATION
1076
- if (marker.data.type && marker.data.type === "overview") {
1077
- marker_location = this._getMapCenter(true);
1078
- } else {
1079
- marker_location = marker.location();
1080
- }
1558
+ const center = this._getMapCenter();
1559
+ for (const [i, marker] of this._markers.entries()) {
1560
+ // a marker with no location object at all is not navigable
1561
+ if (!marker.data.location) {
1562
+ continue;
1563
+ }
1564
+ let calculated_zoom: number | undefined;
1565
+
1566
+ // MARKER LOCATION — the overview fits around the current view
1567
+ // rather than a point on the map
1568
+ const marker_location =
1569
+ marker.data.type && marker.data.type === "overview" ? center : marker.location();
1570
+ if (!marker_location) {
1571
+ continue;
1572
+ }
1081
1573
 
1082
- // Fit-based zoom: zoom the view to this marker and neighbors
1083
- const prev_marker =
1084
- i > 0 ? this._markers[i - 1].location() : this._getMapCenter(true);
1085
- const next_marker =
1086
- i < this._markers.length - 1
1087
- ? this._markers[i + 1].location()
1088
- : this._getMapCenter(true);
1089
-
1090
- const prev_marker_zoom = this._calculateZoomChange(prev_marker, marker_location);
1091
- const next_marker_zoom = this._calculateZoomChange(next_marker, marker_location);
1092
-
1093
- if (prev_marker_zoom && prev_marker_zoom < next_marker_zoom) {
1094
- calculated_zoom = prev_marker_zoom;
1095
- } else if (next_marker_zoom) {
1096
- calculated_zoom = next_marker_zoom;
1097
- } else {
1098
- calculated_zoom = prev_marker_zoom;
1099
- }
1574
+ // Fit-based zoom: zoom the view to this marker and neighbors
1575
+ const prev_marker = i > 0 ? this._markers[i - 1].location() : center;
1576
+ const next_marker =
1577
+ i < this._markers.length - 1 ? this._markers[i + 1].location() : center;
1578
+
1579
+ const prev_marker_zoom = prev_marker
1580
+ ? this._calculateZoomChange(prev_marker, marker_location)
1581
+ : undefined;
1582
+ const next_marker_zoom = next_marker
1583
+ ? this._calculateZoomChange(next_marker, marker_location)
1584
+ : undefined;
1585
+
1586
+ if (prev_marker_zoom && prev_marker_zoom < (next_marker_zoom ?? Infinity)) {
1587
+ calculated_zoom = prev_marker_zoom;
1588
+ } else if (next_marker_zoom) {
1589
+ calculated_zoom = next_marker_zoom;
1590
+ } else {
1591
+ calculated_zoom = prev_marker_zoom;
1592
+ }
1100
1593
 
1101
- if (
1102
- this.options.map_center_offset &&
1103
- (this.options.map_center_offset.left !== 0 ||
1104
- this.options.map_center_offset.top !== 0)
1105
- ) {
1106
- calculated_zoom = calculated_zoom - 1;
1107
- }
1594
+ if (calculated_zoom === undefined) {
1595
+ continue;
1596
+ }
1108
1597
 
1109
- marker.data.location.zoom = calculated_zoom;
1598
+ if (
1599
+ this.options.map_center_offset &&
1600
+ (this.options.map_center_offset.left !== 0 ||
1601
+ this.options.map_center_offset.top !== 0)
1602
+ ) {
1603
+ calculated_zoom = calculated_zoom - 1;
1110
1604
  }
1605
+
1606
+ this._marker_zooms[i] = calculated_zoom;
1111
1607
  }
1112
1608
  }
1113
1609
 
@@ -1149,25 +1645,32 @@ export default class OpenLayers extends Map {
1149
1645
  this._map.addLayer(line);
1150
1646
  }
1151
1647
 
1152
- _addToLine(line: VectorLayer, d: LinePoint): void {
1648
+ _addToLine(line: VectorLayer, d: StorymapSlide): void {
1153
1649
  // Append a point to the line's geometry
1154
1650
  const source = line.getSource();
1651
+ if (!source) {
1652
+ return;
1653
+ }
1155
1654
  let feature = source.getFeatures()[0];
1156
1655
  if (!feature) {
1157
1656
  feature = new Feature({ geometry: new LineString([]) });
1158
1657
  source.addFeature(feature);
1159
1658
  }
1160
1659
  const coords = feature.getGeometry().getCoordinates();
1161
- let lon = d.location.lon;
1660
+ let lon = d.location?.lon;
1661
+ const lat = d.location?.lat;
1662
+ if (lat === undefined || lon === undefined) {
1663
+ return;
1664
+ }
1162
1665
  // unwrap dateline crossings against the previous point (issue #381).
1163
1666
  // NB: read the previous longitude straight from meters — toLonLat()
1164
1667
  // wraps into [-180, 180] and would collapse already-unwrapped values.
1165
- if (coords.length > 0 && lon !== undefined) {
1668
+ if (coords.length > 0) {
1166
1669
  const last_lon = coords[coords.length - 1][0] / 111319.49079327358;
1167
1670
  while (lon - last_lon > 180) lon -= 360;
1168
1671
  while (lon - last_lon < -180) lon += 360;
1169
1672
  }
1170
- coords.push(this._toViewCoords({ lat: d.location.lat, lon }));
1673
+ coords.push(this._toViewCoords({ lat, lon }));
1171
1674
  feature.getGeometry().setCoordinates(coords);
1172
1675
  }
1173
1676
 
@@ -1198,6 +1701,9 @@ export default class OpenLayers extends Map {
1198
1701
  const unwrapped = pts.map((p, i) => [lons[i], p[1]]);
1199
1702
  const view_coords = this._markerCoordsToViewCoords(unwrapped);
1200
1703
  const source = line.getSource();
1704
+ if (!source) {
1705
+ return;
1706
+ }
1201
1707
 
1202
1708
  const setGeometry = (coords: number[][]) => {
1203
1709
  source.clear();
@@ -1267,7 +1773,7 @@ export default class OpenLayers extends Map {
1267
1773
  const suffix = grow_len >= 1 ? view_coords.slice(grow_len - 1) : null;
1268
1774
  const suffix_total = suffix ? this._pathLength(suffix) : 0;
1269
1775
 
1270
- const easing = this.options.ease as ((t: number) => number) | undefined;
1776
+ const easing = this._easing;
1271
1777
  const start_time = performance.now();
1272
1778
  if (prefix) {
1273
1779
  // Seed the already-traveled prefix so it stays red from frame 0
@@ -1353,7 +1859,7 @@ export default class OpenLayers extends Map {
1353
1859
  this._map.getView().animate({
1354
1860
  center: this._toViewCoords(loc),
1355
1861
  duration: this.options.duration,
1356
- easing: this.options.ease as ((t: number) => number) | undefined,
1862
+ easing: this._easing,
1357
1863
  });
1358
1864
  }
1359
1865
 
@@ -1361,15 +1867,33 @@ export default class OpenLayers extends Map {
1361
1867
  this._map.getView().animate({
1362
1868
  zoom: z,
1363
1869
  duration: this.options.duration,
1364
- easing: this.options.ease as ((t: number) => number) | undefined,
1870
+ easing: this._easing,
1365
1871
  });
1366
1872
  }
1367
1873
 
1368
1874
  _viewTo(loc: StorymapSlideLocation, opts?: ViewToOptions): void {
1875
+ // Image region stops (StrollView-style): in image mode the view is
1876
+ // EPSG:4326 where coordinates are raw image pixels — fit the xywh
1877
+ // region instead of flying to a point. Everything else (geo maps,
1878
+ // absent/invalid regions) keeps the point behavior unchanged.
1879
+ const is_image_space = this._map.getView().getProjection().getCode() === "EPSG:4326";
1880
+ const region =
1881
+ is_image_space && Array.isArray(loc.region) && loc.region.length === 4
1882
+ ? loc.region
1883
+ : null;
1884
+ if (region) {
1885
+ this._fitRegion(region, opts);
1886
+ return;
1887
+ }
1888
+
1889
+ const start = this._latLngOf(loc);
1890
+ if (!start) {
1891
+ return;
1892
+ }
1369
1893
  let _animate = true,
1370
1894
  _duration = this.options.duration,
1371
1895
  _zoom = this._getMapZoom(),
1372
- _location: LatLngLiteral = { lat: loc.lat, lon: loc.lon };
1896
+ _location: LatLngLiteral = start;
1373
1897
 
1374
1898
  // Show Active Line
1375
1899
  if (!this.options.map_as_image) {
@@ -1404,11 +1928,15 @@ export default class OpenLayers extends Map {
1404
1928
  center: this._toViewCoords(_location),
1405
1929
  zoom: _zoom,
1406
1930
  duration: _animate ? _duration : 0,
1407
- easing: this.options.ease as ((t: number) => number) | undefined,
1931
+ easing: this._easing,
1408
1932
  });
1409
1933
 
1410
1934
  if (this._mini_map && this.options.width > this.options.skinny_size) {
1411
- if (_zoom - 1 <= this.zoom_min_max.min) {
1935
+ // zoom_min_max stays null until the markers are measured (first
1936
+ // loadend), and `_zoom - 1 <= null` coerces to `<= 0` — the
1937
+ // minimap state was decided by an accidental coercion. Treat an
1938
+ // unmeasured range as "not zoomed out", i.e. keep it expanded.
1939
+ if (this.zoom_min_max.min !== null && _zoom - 1 <= this.zoom_min_max.min) {
1412
1940
  this._mini_map.setCollapsed(true);
1413
1941
  } else {
1414
1942
  this._mini_map.setCollapsed(false);
@@ -1416,6 +1944,36 @@ export default class OpenLayers extends Map {
1416
1944
  }
1417
1945
  }
1418
1946
 
1947
+ /**
1948
+ * Image region stop: fit the xywh bbox ([x, y, w, h] image pixels —
1949
+ * the EPSG:4326 image space) with the shared animation options, then
1950
+ * keep the minimap collapse state in sync like `_viewTo`.
1951
+ */
1952
+ _fitRegion(region: [number, number, number, number], opts?: ViewToOptions): void {
1953
+ const [x, y, w, h] = region;
1954
+ if (!(w > 0) || !(h > 0)) {
1955
+ return;
1956
+ }
1957
+ let _animate = true;
1958
+ let _duration = this.options.duration;
1959
+ if (opts && opts.duration !== undefined) {
1960
+ if (opts.duration === 0) {
1961
+ _animate = false;
1962
+ } else {
1963
+ _duration = opts.duration;
1964
+ }
1965
+ }
1966
+ this._map.getView().fit([x, y, x + w, y + h], {
1967
+ size: this._map.getSize(),
1968
+ padding: this._opaquePanelPadding(),
1969
+ duration: _animate ? _duration : 0,
1970
+ easing: this._easing,
1971
+ });
1972
+ if (this._mini_map && this.options.width > this.options.skinny_size) {
1973
+ this._mini_map.setCollapsed(true);
1974
+ }
1975
+ }
1976
+
1419
1977
  _toViewCoords(loc: LatLngLiteral): number[] {
1420
1978
  const is_image_space = this._map.getView().getProjection().getCode() === "EPSG:4326";
1421
1979
  if (is_image_space) return [loc.lon, loc.lat];
@@ -1428,25 +1986,29 @@ export default class OpenLayers extends Map {
1428
1986
  return this._map.getView().getZoom() || 0;
1429
1987
  }
1430
1988
 
1431
- _getMapCenter(offset?: boolean): LatLngLiteral {
1432
- const center = toLonLat(
1433
- this._map.getView().getCenter(),
1434
- this._map.getView().getProjection(),
1435
- );
1436
- return { lat: center[1], lon: center[0] };
1989
+ // Note: there is deliberately no offset-aware variant. The exposed
1990
+ // center is panel-shifted (see the migration guide); a previous
1991
+ // `offset?: boolean` parameter promised a correction it never applied.
1992
+ _getMapCenter(): LatLngLiteral {
1993
+ const view = this._map.getView();
1994
+ const center = view.getCenter() ?? [0, 0];
1995
+ const [lon, lat] = toLonLat(center, view.getProjection());
1996
+ return { lat: lat, lon: lon };
1437
1997
  }
1438
1998
 
1439
1999
  _getMapCenterOffset(location: LatLngLiteral, zoom: number): LatLngLiteral {
1440
- // Offset the center by map_center_offset pixels at the given zoom
2000
+ // Offset the center by map_center_offset pixels at the given zoom.
2001
+ // A null map_center_offset means "no offset" (the default).
2002
+ const offset = this.options.map_center_offset;
2003
+ if (!offset || (offset.left === 0 && offset.top === 0)) {
2004
+ return location;
2005
+ }
1441
2006
  const view = this._map.getView();
1442
2007
  const projection = view.getProjection();
1443
2008
  const center = this._toViewCoords(location);
1444
2009
  const resolution = view.getResolutionForZoom(zoom);
1445
2010
  return this._fromViewCoords(
1446
- [
1447
- center[0] - this.options.map_center_offset.left * resolution,
1448
- center[1] + this.options.map_center_offset.top * resolution,
1449
- ],
2011
+ [center[0] - offset.left * resolution, center[1] + offset.top * resolution],
1450
2012
  projection,
1451
2013
  );
1452
2014
  }
@@ -1459,13 +2021,48 @@ export default class OpenLayers extends Map {
1459
2021
  return { lat: c[1], lon: (coord[0] / 6378137) * (180 / Math.PI) };
1460
2022
  }
1461
2023
 
2024
+ /**
2025
+ * May a tile-backed layer (base map, overlay, minimap, overview) be
2026
+ * created right now? Under `consent_required` that is only true once the
2027
+ * visitor has allowed the tile service. This was the same five-line
2028
+ * predicate at five call sites.
2029
+ */
2030
+ private _tilesAllowed(): boolean {
2031
+ const consent = consentManagerOf(this.options);
2032
+ if (!this.options.consent_required || !consent) {
2033
+ return true;
2034
+ }
2035
+ return consent.isGranted(tileService().key);
2036
+ }
2037
+
2038
+ /** A slide's {lat, lon} when both are real numbers, else null. */
2039
+ private _latLngOf(loc: StorymapSlideLocation | null | undefined): LatLngLiteral | null {
2040
+ if (!loc || typeof loc.lat !== "number" || typeof loc.lon !== "number") {
2041
+ return null;
2042
+ }
2043
+ return { lat: loc.lat, lon: loc.lon };
2044
+ }
2045
+
2046
+ /**
2047
+ * The map's pixel size, with a never-zero fallback.
2048
+ *
2049
+ * `getSize()` returns undefined until the map has a size, so the six
2050
+ * call sites that fitted an extent to the viewport all repeated the same
2051
+ * `(size[0] || 1)` guard — which silently fitted against a 1px viewport
2052
+ * rather than bailing out.
2053
+ */
2054
+ private _viewportSize(): [number, number] {
2055
+ const size = this._map?.getSize();
2056
+ return [size?.[0] || 1, size?.[1] || 1];
2057
+ }
2058
+
1462
2059
  _getBoundsZoom(
1463
2060
  origin: LatLngLiteral,
1464
2061
  destination: LatLngLiteral,
1465
2062
  correct_for_center?: boolean,
1466
2063
  ): number {
1467
2064
  const coords = [
1468
- [origin.lon !== undefined ? origin.lon : origin.lng, origin.lat],
2065
+ [origin.lon, origin.lat],
1469
2066
  [destination.lon, destination.lat],
1470
2067
  ];
1471
2068
  const view_coords = this._markerCoordsToViewCoords(coords);
@@ -1549,28 +2146,34 @@ export default class OpenLayers extends Map {
1549
2146
  center: center_view,
1550
2147
  zoom: view_zoom,
1551
2148
  duration: duration ?? this._transition_duration,
1552
- easing: this.options.ease as ((t: number) => number) | undefined,
2149
+ easing: this._easing,
1553
2150
  });
1554
2151
  }
1555
- } else if (this.options.map_type === "iiif" && this.options.map_as_image) {
1556
- const source = this._tile_layer?.getSource();
2152
+ } else if (this.isImageSpace()) {
2153
+ const source = this._sourceOf(this._tile_layer);
1557
2154
  if (!source) {
1558
2155
  return;
1559
2156
  }
1560
2157
  const fit = () => {
1561
2158
  try {
1562
- const grid = source.getTileGrid();
2159
+ // a custom layer with a source is not necessarily a tile
2160
+ // source; only tile grids can supply a fit extent
2161
+ const grid = (
2162
+ source as {
2163
+ getTileGrid?: () => { getExtent(): number[] } | null;
2164
+ }
2165
+ ).getTileGrid?.();
1563
2166
  if (grid) {
1564
2167
  // compute the fit target directly and animate once (a
1565
2168
  // fit() followed by setCenter() would cancel the fit
1566
2169
  // animation, issue #465)
1567
2170
  const extent = grid.getExtent();
1568
- const size = this._map.getSize();
1569
- const resolution_x = (extent[2] - extent[0]) / (size[0] || 1);
1570
- const resolution_y = (extent[3] - extent[1]) / (size[1] || 1);
2171
+ const size = this._viewportSize();
2172
+ const resolution_x = (extent[2] - extent[0]) / size[0];
2173
+ const resolution_y = (extent[3] - extent[1]) / size[1];
1571
2174
  const resolution = Math.max(resolution_x, resolution_y);
1572
2175
  const view = this._map.getView();
1573
- const zoom = view.getZoomForResolution(resolution);
2176
+ const zoom = view.getZoomForResolution(resolution) ?? 0;
1574
2177
  const overview_center = this.options.map_overview_center;
1575
2178
  const center_px = overview_center
1576
2179
  ? this._toViewCoords({
@@ -1585,7 +2188,7 @@ export default class OpenLayers extends Map {
1585
2188
  center: this._toViewCoords(offset_location),
1586
2189
  zoom: zoom,
1587
2190
  duration: duration ?? this._transition_duration,
1588
- easing: this.options.ease as ((t: number) => number) | undefined,
2191
+ easing: this._easing,
1589
2192
  });
1590
2193
  }
1591
2194
  } catch (e) {
@@ -1609,14 +2212,14 @@ export default class OpenLayers extends Map {
1609
2212
  if (overview_center && this.bounds_array && this.bounds_array.length) {
1610
2213
  const view_coords = this._markerCoordsToViewCoords(this.bounds_array);
1611
2214
  const extent = boundingExtent(view_coords);
1612
- const size = this._map.getSize();
2215
+ const size = this._viewportSize();
1613
2216
  const resolution = Math.max(
1614
- (extent[2] - extent[0]) / (size[0] || 1),
1615
- (extent[3] - extent[1]) / (size[1] || 1),
2217
+ (extent[2] - extent[0]) / size[0],
2218
+ (extent[3] - extent[1]) / size[1],
1616
2219
  );
1617
2220
  const zoom = Math.max(
1618
2221
  0,
1619
- Math.round(this._map.getView().getZoomForResolution(resolution)) - 1,
2222
+ Math.round(this._map.getView().getZoomForResolution(resolution) ?? 0) - 1,
1620
2223
  );
1621
2224
  const offset_location = this._getMapCenterOffset(
1622
2225
  { lat: overview_center.lat, lon: overview_center.lon },
@@ -1626,7 +2229,7 @@ export default class OpenLayers extends Map {
1626
2229
  center: this._toViewCoords(offset_location),
1627
2230
  zoom: zoom,
1628
2231
  duration: duration ?? this._transition_duration,
1629
- easing: this.options.ease as ((t: number) => number) | undefined,
2232
+ easing: this._easing,
1630
2233
  });
1631
2234
  } else if (
1632
2235
  this.options.map_center_offset &&
@@ -1636,13 +2239,13 @@ export default class OpenLayers extends Map {
1636
2239
  if (this.bounds_array && this.bounds_array.length) {
1637
2240
  const view_coords = this._markerCoordsToViewCoords(this.bounds_array);
1638
2241
  const extent = boundingExtent(view_coords);
1639
- const size = this._map.getSize();
1640
- const resolution_x = (extent[2] - extent[0]) / (size[0] || 1);
1641
- const resolution_y = (extent[3] - extent[1]) / (size[1] || 1);
2242
+ const size = this._viewportSize();
2243
+ const resolution_x = (extent[2] - extent[0]) / size[0];
2244
+ const resolution_y = (extent[3] - extent[1]) / size[1];
1642
2245
  const resolution = Math.max(resolution_x, resolution_y);
1643
2246
  const zoom = Math.max(
1644
2247
  0,
1645
- Math.round(this._map.getView().getZoomForResolution(resolution)) - 1,
2248
+ Math.round(this._map.getView().getZoomForResolution(resolution) ?? 0) - 1,
1646
2249
  );
1647
2250
  const center_px = [(extent[0] + extent[2]) / 2, (extent[1] + extent[3]) / 2];
1648
2251
  const projection = this._map.getView().getProjection();
@@ -1652,7 +2255,7 @@ export default class OpenLayers extends Map {
1652
2255
  center: this._toViewCoords(offset_location),
1653
2256
  zoom: zoom,
1654
2257
  duration: duration ?? this._transition_duration,
1655
- easing: this.options.ease as ((t: number) => number) | undefined,
2258
+ easing: this._easing,
1656
2259
  });
1657
2260
  }
1658
2261
  } else {
@@ -1664,13 +2267,16 @@ export default class OpenLayers extends Map {
1664
2267
  const view = this._map.getView();
1665
2268
  const zoom = view.getZoom();
1666
2269
  if (zoom !== undefined) {
1667
- const center = this._fromViewCoords(view.getCenter(), view.getProjection());
2270
+ const center = this._fromViewCoords(
2271
+ view.getCenter() ?? [0, 0],
2272
+ view.getProjection(),
2273
+ );
1668
2274
  const offset_location = this._getMapCenterOffset(center, zoom);
1669
2275
  view.animate({
1670
2276
  center: this._toViewCoords(offset_location),
1671
2277
  zoom: zoom,
1672
2278
  duration: duration ?? this._transition_duration,
1673
- easing: this.options.ease as ((t: number) => number) | undefined,
2279
+ easing: this._easing,
1674
2280
  });
1675
2281
  }
1676
2282
  }
@@ -1690,14 +2296,7 @@ export default class OpenLayers extends Map {
1690
2296
  if (!this._mini_map) return;
1691
2297
  const overview = this._mini_map.getOverviewMap();
1692
2298
  overview.getLayers().clear();
1693
- const consent = consentManagerOf(this.options);
1694
- const tile_service = consentMessage("consent_service_tiles", "map tiles");
1695
- const tiles_allowed = !(
1696
- this.options.consent_required &&
1697
- consent &&
1698
- !consent.isGranted(tile_service)
1699
- );
1700
- if (!tiles_allowed) {
2299
+ if (!this._tilesAllowed()) {
1701
2300
  this._tile_layer_mini = null;
1702
2301
  return;
1703
2302
  }
@@ -1707,13 +2306,12 @@ export default class OpenLayers extends Map {
1707
2306
  const zoomify_pyramid = is_zoomify ? this._zoomifyPyramid() : null;
1708
2307
  if (zoomify_pyramid) {
1709
2308
  const raw_size = overview.getSize();
1710
- const size =
1711
- raw_size && raw_size[0] >= 50 && raw_size[1] >= 50 ? raw_size : [150, 100];
2309
+ const size = raw_size && raw_size[0] >= 50 && raw_size[1] >= 50 ? raw_size : [150, 100];
1712
2310
  overview.getView().fit(zoomify_pyramid.extent, { size: size });
1713
2311
  } else if (this.bounds_array && this.bounds_array.length) {
1714
2312
  this._fitView(overview, this.bounds_array, 0, this._bboxExtent() !== null);
1715
2313
  }
1716
- if (this.options.map_type === "iiif" && this.options.map_as_image) {
2314
+ if (this.isImageSpace()) {
1717
2315
  this._fitMiniMapToImage();
1718
2316
  }
1719
2317
  }
@@ -1733,13 +2331,7 @@ export default class OpenLayers extends Map {
1733
2331
  if (this._tile_layer) {
1734
2332
  this._map.removeLayer(this._tile_layer);
1735
2333
  }
1736
- const consent = consentManagerOf(this.options);
1737
- const tile_service = consentMessage("consent_service_tiles", "map tiles");
1738
- if (!(
1739
- this.options.consent_required &&
1740
- consent &&
1741
- !consent.isGranted(tile_service)
1742
- )) {
2334
+ if (this._tilesAllowed()) {
1743
2335
  this._tile_layer = this._createTileLayer(this.options.map_type);
1744
2336
  this._tile_layer.setZIndex(0);
1745
2337
  this._map.addLayer(this._tile_layer);
@@ -1774,19 +2366,36 @@ export default class OpenLayers extends Map {
1774
2366
  break;
1775
2367
  case "map_bbox": {
1776
2368
  // recreate the view so the new extent constraint applies,
1777
- // preserving center and zoom
2369
+ // preserving center and zoom. The replacement has to
2370
+ // rebuild the *same* view configuration _createMap() uses —
2371
+ // dropping constrainOnlyCenter (which _createMap sets
2372
+ // deliberately, see above), the image zoom ladder or the
2373
+ // legacy zoomify multiWorld flag silently reintroduced the
2374
+ // strict extent constraint this code went out of its way
2375
+ // to avoid, and broke image-mode zoom (issue #465).
1778
2376
  const view = this._map.getView();
1779
2377
  const center = view.getCenter();
1780
2378
  const zoom = view.getZoom();
1781
2379
  const extent = this._bboxExtent();
2380
+ // must match _createMap's test exactly: a *georeferenced*
2381
+ // IIIF layer (map_as_image false) is a mercator map, so
2382
+ // testing map_type alone handed it the image-pixel view
2383
+ const is_image_map = this.options.map_type === "iiif" && this.isImageSpace();
2384
+ const user_view = this.options.map_options?.view as
2385
+ Record<string, unknown> | undefined;
1782
2386
  this._map.setView(
1783
2387
  new View({
1784
- projection: view.getProjection(),
2388
+ projection: is_image_map ? "EPSG:4326" : "EPSG:3857",
1785
2389
  center: center,
1786
2390
  zoom: zoom,
1787
2391
  minZoom: view.getMinZoom(),
1788
2392
  maxZoom: view.getMaxZoom(),
1789
- ...(extent ? { extent: extent } : {}),
2393
+ ...(is_image_map
2394
+ ? { multiWorld: true, resolutions: IMAGE_RESOLUTIONS }
2395
+ : {}),
2396
+ ...(this.options.map_type === "zoomify" ? { multiWorld: true } : {}),
2397
+ ...(extent ? { extent: extent, constrainOnlyCenter: true } : {}),
2398
+ ...((user_view ?? {}) as Record<string, unknown>),
1790
2399
  }),
1791
2400
  );
1792
2401
  break;
@@ -1865,7 +2474,11 @@ export default class OpenLayers extends Map {
1865
2474
  * stale positions after a size change.
1866
2475
  */
1867
2476
  _setViewInstant(loc: StorymapSlideLocation, zoom: number): void {
1868
- let _location: LatLngLiteral = { lat: loc.lat, lon: loc.lon };
2477
+ const start = this._latLngOf(loc);
2478
+ if (!start) {
2479
+ return;
2480
+ }
2481
+ let _location: LatLngLiteral = start;
1869
2482
  if (this.options.map_center_offset) {
1870
2483
  _location = this._getMapCenterOffset(_location, zoom);
1871
2484
  }