@projektemacher/storymapjs 0.10.7 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG +431 -19
- package/README.md +168 -17
- package/dist/assets/demo-bWnOyfUw.js +1 -0
- package/dist/assets/harness-C3JUv5Ii.js +1 -0
- package/dist/assets/harness-multi-XZNex3tb.js +1 -0
- package/dist/assets/harness-support-CjfZIVeb.js +1 -0
- package/dist/assets/index-wHPqGrBS.js +1 -0
- package/dist/assets/main-DQ32hWBM.js +17 -0
- package/dist/context.json +23 -9
- package/dist/css/fonts/files/amatic-sc-cyrillic-400-normal.woff2 +0 -0
- package/dist/css/fonts/files/amatic-sc-cyrillic-700-normal.woff2 +0 -0
- package/dist/css/fonts/files/amatic-sc-hebrew-400-normal.woff2 +0 -0
- package/dist/css/fonts/files/amatic-sc-hebrew-700-normal.woff2 +0 -0
- package/dist/css/fonts/files/amatic-sc-vietnamese-400-normal.woff2 +0 -0
- package/dist/css/fonts/files/amatic-sc-vietnamese-700-normal.woff2 +0 -0
- package/dist/css/fonts/files/bitter-cyrillic-100-italic.woff2 +0 -0
- package/dist/css/fonts/files/bitter-cyrillic-100-normal.woff2 +0 -0
- package/dist/css/fonts/files/bitter-cyrillic-200-italic.woff2 +0 -0
- package/dist/css/fonts/files/bitter-cyrillic-200-normal.woff2 +0 -0
- package/dist/css/fonts/files/bitter-cyrillic-300-italic.woff2 +0 -0
- package/dist/css/fonts/files/bitter-cyrillic-300-normal.woff2 +0 -0
- package/dist/css/fonts/files/bitter-cyrillic-400-italic.woff2 +0 -0
- package/dist/css/fonts/files/bitter-cyrillic-400-normal.woff2 +0 -0
- package/dist/css/fonts/files/bitter-cyrillic-500-italic.woff2 +0 -0
- package/dist/css/fonts/files/bitter-cyrillic-500-normal.woff2 +0 -0
- package/dist/css/fonts/files/bitter-cyrillic-600-italic.woff2 +0 -0
- package/dist/css/fonts/files/bitter-cyrillic-600-normal.woff2 +0 -0
- package/dist/css/fonts/files/bitter-cyrillic-700-italic.woff2 +0 -0
- package/dist/css/fonts/files/bitter-cyrillic-700-normal.woff2 +0 -0
- package/dist/css/fonts/files/bitter-cyrillic-800-italic.woff2 +0 -0
- package/dist/css/fonts/files/bitter-cyrillic-800-normal.woff2 +0 -0
- package/dist/css/fonts/files/bitter-cyrillic-900-italic.woff2 +0 -0
- package/dist/css/fonts/files/bitter-cyrillic-900-normal.woff2 +0 -0
- package/dist/css/fonts/files/bitter-cyrillic-ext-100-italic.woff2 +0 -0
- package/dist/css/fonts/files/bitter-cyrillic-ext-100-normal.woff2 +0 -0
- package/dist/css/fonts/files/bitter-cyrillic-ext-200-italic.woff2 +0 -0
- package/dist/css/fonts/files/bitter-cyrillic-ext-200-normal.woff2 +0 -0
- package/dist/css/fonts/files/bitter-cyrillic-ext-300-italic.woff2 +0 -0
- package/dist/css/fonts/files/bitter-cyrillic-ext-300-normal.woff2 +0 -0
- package/dist/css/fonts/files/bitter-cyrillic-ext-400-italic.woff2 +0 -0
- package/dist/css/fonts/files/bitter-cyrillic-ext-400-normal.woff2 +0 -0
- package/dist/css/fonts/files/bitter-cyrillic-ext-500-italic.woff2 +0 -0
- package/dist/css/fonts/files/bitter-cyrillic-ext-500-normal.woff2 +0 -0
- package/dist/css/fonts/files/bitter-cyrillic-ext-600-italic.woff2 +0 -0
- package/dist/css/fonts/files/bitter-cyrillic-ext-600-normal.woff2 +0 -0
- package/dist/css/fonts/files/bitter-cyrillic-ext-700-italic.woff2 +0 -0
- package/dist/css/fonts/files/bitter-cyrillic-ext-700-normal.woff2 +0 -0
- package/dist/css/fonts/files/bitter-cyrillic-ext-800-italic.woff2 +0 -0
- package/dist/css/fonts/files/bitter-cyrillic-ext-800-normal.woff2 +0 -0
- package/dist/css/fonts/files/bitter-cyrillic-ext-900-italic.woff2 +0 -0
- package/dist/css/fonts/files/bitter-cyrillic-ext-900-normal.woff2 +0 -0
- package/dist/css/fonts/files/bitter-cyrillic-ext-wght-italic.woff2 +0 -0
- package/dist/css/fonts/files/bitter-cyrillic-ext-wght-normal.woff2 +0 -0
- package/dist/css/fonts/files/bitter-cyrillic-wght-italic.woff2 +0 -0
- package/dist/css/fonts/files/bitter-cyrillic-wght-normal.woff2 +0 -0
- package/dist/css/fonts/files/bitter-vietnamese-100-italic.woff2 +0 -0
- package/dist/css/fonts/files/bitter-vietnamese-100-normal.woff2 +0 -0
- package/dist/css/fonts/files/bitter-vietnamese-200-italic.woff2 +0 -0
- package/dist/css/fonts/files/bitter-vietnamese-200-normal.woff2 +0 -0
- package/dist/css/fonts/files/bitter-vietnamese-300-italic.woff2 +0 -0
- package/dist/css/fonts/files/bitter-vietnamese-300-normal.woff2 +0 -0
- package/dist/css/fonts/files/bitter-vietnamese-400-italic.woff2 +0 -0
- package/dist/css/fonts/files/bitter-vietnamese-400-normal.woff2 +0 -0
- package/dist/css/fonts/files/bitter-vietnamese-500-italic.woff2 +0 -0
- package/dist/css/fonts/files/bitter-vietnamese-500-normal.woff2 +0 -0
- package/dist/css/fonts/files/bitter-vietnamese-600-italic.woff2 +0 -0
- package/dist/css/fonts/files/bitter-vietnamese-600-normal.woff2 +0 -0
- package/dist/css/fonts/files/bitter-vietnamese-700-italic.woff2 +0 -0
- package/dist/css/fonts/files/bitter-vietnamese-700-normal.woff2 +0 -0
- package/dist/css/fonts/files/bitter-vietnamese-800-italic.woff2 +0 -0
- package/dist/css/fonts/files/bitter-vietnamese-800-normal.woff2 +0 -0
- package/dist/css/fonts/files/bitter-vietnamese-900-italic.woff2 +0 -0
- package/dist/css/fonts/files/bitter-vietnamese-900-normal.woff2 +0 -0
- package/dist/css/fonts/files/bitter-vietnamese-wght-italic.woff2 +0 -0
- package/dist/css/fonts/files/bitter-vietnamese-wght-normal.woff2 +0 -0
- package/dist/css/fonts/files/cairo-arabic-200-normal.woff2 +0 -0
- package/dist/css/fonts/files/cairo-arabic-300-normal.woff2 +0 -0
- package/dist/css/fonts/files/cairo-arabic-400-normal.woff2 +0 -0
- package/dist/css/fonts/files/cairo-arabic-500-normal.woff2 +0 -0
- package/dist/css/fonts/files/cairo-arabic-600-normal.woff2 +0 -0
- package/dist/css/fonts/files/cairo-arabic-700-normal.woff2 +0 -0
- package/dist/css/fonts/files/cairo-arabic-800-normal.woff2 +0 -0
- package/dist/css/fonts/files/cairo-arabic-900-normal.woff2 +0 -0
- package/dist/css/fonts/files/cairo-latin-200-normal.woff2 +0 -0
- package/dist/css/fonts/files/cairo-latin-300-normal.woff2 +0 -0
- package/dist/css/fonts/files/cairo-latin-400-normal.woff2 +0 -0
- package/dist/css/fonts/files/cairo-latin-500-normal.woff2 +0 -0
- package/dist/css/fonts/files/cairo-latin-600-normal.woff2 +0 -0
- package/dist/css/fonts/files/cairo-latin-700-normal.woff2 +0 -0
- package/dist/css/fonts/files/cairo-latin-800-normal.woff2 +0 -0
- package/dist/css/fonts/files/cairo-latin-900-normal.woff2 +0 -0
- package/dist/css/fonts/files/cairo-latin-ext-200-normal.woff2 +0 -0
- package/dist/css/fonts/files/cairo-latin-ext-300-normal.woff2 +0 -0
- package/dist/css/fonts/files/cairo-latin-ext-400-normal.woff2 +0 -0
- package/dist/css/fonts/files/cairo-latin-ext-500-normal.woff2 +0 -0
- package/dist/css/fonts/files/cairo-latin-ext-600-normal.woff2 +0 -0
- package/dist/css/fonts/files/cairo-latin-ext-700-normal.woff2 +0 -0
- package/dist/css/fonts/files/cairo-latin-ext-800-normal.woff2 +0 -0
- package/dist/css/fonts/files/cairo-latin-ext-900-normal.woff2 +0 -0
- package/dist/css/fonts/files/dancing-script-vietnamese-wght-normal.woff2 +0 -0
- package/dist/css/fonts/files/eb-garamond-cyrillic-ext-wght-italic.woff2 +0 -0
- package/dist/css/fonts/files/eb-garamond-cyrillic-ext-wght-normal.woff2 +0 -0
- package/dist/css/fonts/files/eb-garamond-cyrillic-wght-italic.woff2 +0 -0
- package/dist/css/fonts/files/eb-garamond-cyrillic-wght-normal.woff2 +0 -0
- package/dist/css/fonts/files/eb-garamond-greek-ext-wght-italic.woff2 +0 -0
- package/dist/css/fonts/files/eb-garamond-greek-ext-wght-normal.woff2 +0 -0
- package/dist/css/fonts/files/eb-garamond-greek-wght-italic.woff2 +0 -0
- package/dist/css/fonts/files/eb-garamond-greek-wght-normal.woff2 +0 -0
- package/dist/css/fonts/files/eb-garamond-vietnamese-wght-italic.woff2 +0 -0
- package/dist/css/fonts/files/eb-garamond-vietnamese-wght-normal.woff2 +0 -0
- package/dist/css/fonts/files/gentium-book-plus-cyrillic-400-italic.woff2 +0 -0
- package/dist/css/fonts/files/gentium-book-plus-cyrillic-400-normal.woff2 +0 -0
- package/dist/css/fonts/files/gentium-book-plus-cyrillic-700-italic.woff2 +0 -0
- package/dist/css/fonts/files/gentium-book-plus-cyrillic-700-normal.woff2 +0 -0
- package/dist/css/fonts/files/gentium-book-plus-cyrillic-ext-400-italic.woff2 +0 -0
- package/dist/css/fonts/files/gentium-book-plus-cyrillic-ext-400-normal.woff2 +0 -0
- package/dist/css/fonts/files/gentium-book-plus-cyrillic-ext-700-italic.woff2 +0 -0
- package/dist/css/fonts/files/gentium-book-plus-cyrillic-ext-700-normal.woff2 +0 -0
- package/dist/css/fonts/files/gentium-book-plus-greek-400-italic.woff2 +0 -0
- package/dist/css/fonts/files/gentium-book-plus-greek-400-normal.woff2 +0 -0
- package/dist/css/fonts/files/gentium-book-plus-greek-700-italic.woff2 +0 -0
- package/dist/css/fonts/files/gentium-book-plus-greek-700-normal.woff2 +0 -0
- package/dist/css/fonts/files/gentium-book-plus-greek-ext-400-italic.woff2 +0 -0
- package/dist/css/fonts/files/gentium-book-plus-greek-ext-400-normal.woff2 +0 -0
- package/dist/css/fonts/files/gentium-book-plus-greek-ext-700-italic.woff2 +0 -0
- package/dist/css/fonts/files/gentium-book-plus-greek-ext-700-normal.woff2 +0 -0
- package/dist/css/fonts/files/gentium-book-plus-vietnamese-400-italic.woff2 +0 -0
- package/dist/css/fonts/files/gentium-book-plus-vietnamese-400-normal.woff2 +0 -0
- package/dist/css/fonts/files/gentium-book-plus-vietnamese-700-italic.woff2 +0 -0
- package/dist/css/fonts/files/gentium-book-plus-vietnamese-700-normal.woff2 +0 -0
- package/dist/css/fonts/files/old-standard-tt-cyrillic-400-italic.woff2 +0 -0
- package/dist/css/fonts/files/old-standard-tt-cyrillic-400-normal.woff2 +0 -0
- package/dist/css/fonts/files/old-standard-tt-cyrillic-700-normal.woff2 +0 -0
- package/dist/css/fonts/files/old-standard-tt-cyrillic-ext-400-italic.woff2 +0 -0
- package/dist/css/fonts/files/old-standard-tt-cyrillic-ext-400-normal.woff2 +0 -0
- package/dist/css/fonts/files/old-standard-tt-cyrillic-ext-700-normal.woff2 +0 -0
- package/dist/css/fonts/files/old-standard-tt-vietnamese-400-italic.woff2 +0 -0
- package/dist/css/fonts/files/old-standard-tt-vietnamese-400-normal.woff2 +0 -0
- package/dist/css/fonts/files/old-standard-tt-vietnamese-700-normal.woff2 +0 -0
- package/dist/css/fonts/files/open-sans-cyrillic-ext-wght-italic.woff2 +0 -0
- package/dist/css/fonts/files/open-sans-cyrillic-ext-wght-normal.woff2 +0 -0
- package/dist/css/fonts/files/open-sans-cyrillic-wght-italic.woff2 +0 -0
- package/dist/css/fonts/files/open-sans-cyrillic-wght-normal.woff2 +0 -0
- package/dist/css/fonts/files/open-sans-greek-ext-wght-italic.woff2 +0 -0
- package/dist/css/fonts/files/open-sans-greek-ext-wght-normal.woff2 +0 -0
- package/dist/css/fonts/files/open-sans-greek-wght-italic.woff2 +0 -0
- package/dist/css/fonts/files/open-sans-greek-wght-normal.woff2 +0 -0
- package/dist/css/fonts/files/open-sans-hebrew-wght-italic.woff2 +0 -0
- package/dist/css/fonts/files/open-sans-hebrew-wght-normal.woff2 +0 -0
- package/dist/css/fonts/files/open-sans-vietnamese-wght-italic.woff2 +0 -0
- package/dist/css/fonts/files/open-sans-vietnamese-wght-normal.woff2 +0 -0
- package/dist/css/fonts/files/playfair-display-cyrillic-wght-italic.woff2 +0 -0
- package/dist/css/fonts/files/playfair-display-cyrillic-wght-normal.woff2 +0 -0
- package/dist/css/fonts/files/playfair-display-sc-cyrillic-400-italic.woff2 +0 -0
- package/dist/css/fonts/files/playfair-display-sc-cyrillic-400-normal.woff2 +0 -0
- package/dist/css/fonts/files/playfair-display-sc-cyrillic-700-italic.woff2 +0 -0
- package/dist/css/fonts/files/playfair-display-sc-cyrillic-700-normal.woff2 +0 -0
- package/dist/css/fonts/files/playfair-display-sc-cyrillic-900-italic.woff2 +0 -0
- package/dist/css/fonts/files/playfair-display-sc-cyrillic-900-normal.woff2 +0 -0
- package/dist/css/fonts/files/playfair-display-sc-vietnamese-400-italic.woff2 +0 -0
- package/dist/css/fonts/files/playfair-display-sc-vietnamese-400-normal.woff2 +0 -0
- package/dist/css/fonts/files/playfair-display-sc-vietnamese-700-italic.woff2 +0 -0
- package/dist/css/fonts/files/playfair-display-sc-vietnamese-700-normal.woff2 +0 -0
- package/dist/css/fonts/files/playfair-display-sc-vietnamese-900-italic.woff2 +0 -0
- package/dist/css/fonts/files/playfair-display-sc-vietnamese-900-normal.woff2 +0 -0
- package/dist/css/fonts/files/playfair-display-vietnamese-wght-italic.woff2 +0 -0
- package/dist/css/fonts/files/playfair-display-vietnamese-wght-normal.woff2 +0 -0
- package/dist/css/fonts/files/pt-sans-cyrillic-400-italic.woff2 +0 -0
- package/dist/css/fonts/files/pt-sans-cyrillic-400-normal.woff2 +0 -0
- package/dist/css/fonts/files/pt-sans-cyrillic-700-italic.woff2 +0 -0
- package/dist/css/fonts/files/pt-sans-cyrillic-700-normal.woff2 +0 -0
- package/dist/css/fonts/files/pt-sans-cyrillic-ext-400-italic.woff2 +0 -0
- package/dist/css/fonts/files/pt-sans-cyrillic-ext-400-normal.woff2 +0 -0
- package/dist/css/fonts/files/pt-sans-cyrillic-ext-700-italic.woff2 +0 -0
- package/dist/css/fonts/files/pt-sans-cyrillic-ext-700-normal.woff2 +0 -0
- package/dist/css/fonts/files/pt-sans-narrow-cyrillic-400-normal.woff2 +0 -0
- package/dist/css/fonts/files/pt-sans-narrow-cyrillic-700-normal.woff2 +0 -0
- package/dist/css/fonts/files/pt-sans-narrow-cyrillic-ext-400-normal.woff2 +0 -0
- package/dist/css/fonts/files/pt-sans-narrow-cyrillic-ext-700-normal.woff2 +0 -0
- package/dist/css/fonts/files/pt-serif-cyrillic-400-italic.woff2 +0 -0
- package/dist/css/fonts/files/pt-serif-cyrillic-400-normal.woff2 +0 -0
- package/dist/css/fonts/files/pt-serif-cyrillic-700-italic.woff2 +0 -0
- package/dist/css/fonts/files/pt-serif-cyrillic-700-normal.woff2 +0 -0
- package/dist/css/fonts/files/pt-serif-cyrillic-ext-400-italic.woff2 +0 -0
- package/dist/css/fonts/files/pt-serif-cyrillic-ext-400-normal.woff2 +0 -0
- package/dist/css/fonts/files/pt-serif-cyrillic-ext-700-italic.woff2 +0 -0
- package/dist/css/fonts/files/pt-serif-cyrillic-ext-700-normal.woff2 +0 -0
- package/dist/css/fonts/files/raleway-cyrillic-ext-wght-italic.woff2 +0 -0
- package/dist/css/fonts/files/raleway-cyrillic-ext-wght-normal.woff2 +0 -0
- package/dist/css/fonts/files/raleway-cyrillic-wght-italic.woff2 +0 -0
- package/dist/css/fonts/files/raleway-cyrillic-wght-normal.woff2 +0 -0
- package/dist/css/fonts/files/raleway-vietnamese-wght-italic.woff2 +0 -0
- package/dist/css/fonts/files/raleway-vietnamese-wght-normal.woff2 +0 -0
- package/dist/css/fonts/files/roboto-slab-cyrillic-ext-wght-normal.woff2 +0 -0
- package/dist/css/fonts/files/roboto-slab-cyrillic-wght-normal.woff2 +0 -0
- package/dist/css/fonts/files/roboto-slab-greek-ext-wght-normal.woff2 +0 -0
- package/dist/css/fonts/files/roboto-slab-greek-wght-normal.woff2 +0 -0
- package/dist/css/fonts/files/roboto-slab-vietnamese-wght-normal.woff2 +0 -0
- package/dist/css/fonts/files/ubuntu-cyrillic-300-italic.woff2 +0 -0
- package/dist/css/fonts/files/ubuntu-cyrillic-300-normal.woff2 +0 -0
- package/dist/css/fonts/files/ubuntu-cyrillic-400-italic.woff2 +0 -0
- package/dist/css/fonts/files/ubuntu-cyrillic-400-normal.woff2 +0 -0
- package/dist/css/fonts/files/ubuntu-cyrillic-500-italic.woff2 +0 -0
- package/dist/css/fonts/files/ubuntu-cyrillic-500-normal.woff2 +0 -0
- package/dist/css/fonts/files/ubuntu-cyrillic-700-italic.woff2 +0 -0
- package/dist/css/fonts/files/ubuntu-cyrillic-700-normal.woff2 +0 -0
- package/dist/css/fonts/files/ubuntu-cyrillic-ext-300-italic.woff2 +0 -0
- package/dist/css/fonts/files/ubuntu-cyrillic-ext-300-normal.woff2 +0 -0
- package/dist/css/fonts/files/ubuntu-cyrillic-ext-400-italic.woff2 +0 -0
- package/dist/css/fonts/files/ubuntu-cyrillic-ext-400-normal.woff2 +0 -0
- package/dist/css/fonts/files/ubuntu-cyrillic-ext-500-italic.woff2 +0 -0
- package/dist/css/fonts/files/ubuntu-cyrillic-ext-500-normal.woff2 +0 -0
- package/dist/css/fonts/files/ubuntu-cyrillic-ext-700-italic.woff2 +0 -0
- package/dist/css/fonts/files/ubuntu-cyrillic-ext-700-normal.woff2 +0 -0
- package/dist/css/fonts/files/ubuntu-greek-300-italic.woff2 +0 -0
- package/dist/css/fonts/files/ubuntu-greek-300-normal.woff2 +0 -0
- package/dist/css/fonts/files/ubuntu-greek-400-italic.woff2 +0 -0
- package/dist/css/fonts/files/ubuntu-greek-400-normal.woff2 +0 -0
- package/dist/css/fonts/files/ubuntu-greek-500-italic.woff2 +0 -0
- package/dist/css/fonts/files/ubuntu-greek-500-normal.woff2 +0 -0
- package/dist/css/fonts/files/ubuntu-greek-700-italic.woff2 +0 -0
- package/dist/css/fonts/files/ubuntu-greek-700-normal.woff2 +0 -0
- package/dist/css/fonts/files/ubuntu-greek-ext-300-italic.woff2 +0 -0
- package/dist/css/fonts/files/ubuntu-greek-ext-300-normal.woff2 +0 -0
- package/dist/css/fonts/files/ubuntu-greek-ext-400-italic.woff2 +0 -0
- package/dist/css/fonts/files/ubuntu-greek-ext-400-normal.woff2 +0 -0
- package/dist/css/fonts/files/ubuntu-greek-ext-500-italic.woff2 +0 -0
- package/dist/css/fonts/files/ubuntu-greek-ext-500-normal.woff2 +0 -0
- package/dist/css/fonts/files/ubuntu-greek-ext-700-italic.woff2 +0 -0
- package/dist/css/fonts/files/ubuntu-greek-ext-700-normal.woff2 +0 -0
- package/dist/css/fonts/files/unica-one-vietnamese-400-normal.woff2 +0 -0
- package/dist/css/fonts/files/vollkorn-cyrillic-ext-wght-italic.woff2 +0 -0
- package/dist/css/fonts/files/vollkorn-cyrillic-ext-wght-normal.woff2 +0 -0
- package/dist/css/fonts/files/vollkorn-cyrillic-wght-italic.woff2 +0 -0
- package/dist/css/fonts/files/vollkorn-cyrillic-wght-normal.woff2 +0 -0
- package/dist/css/fonts/files/vollkorn-greek-wght-italic.woff2 +0 -0
- package/dist/css/fonts/files/vollkorn-greek-wght-normal.woff2 +0 -0
- package/dist/css/fonts/files/vollkorn-vietnamese-wght-italic.woff2 +0 -0
- package/dist/css/fonts/files/vollkorn-vietnamese-wght-normal.woff2 +0 -0
- package/dist/css/fonts/font.abril-droidsans.css +3 -0
- package/dist/css/fonts/font.amatic-andika.css +57 -0
- package/dist/css/fonts/font.bitter-raleway.css +111 -0
- package/dist/css/fonts/font.cairo.css +312 -0
- package/dist/css/fonts/font.clicker-garamond.css +93 -0
- package/dist/css/fonts/font.dancing-ledger.css +12 -0
- package/dist/css/fonts/font.default.css +183 -0
- package/dist/css/fonts/font.fjalla-average.css +3 -0
- package/dist/css/fonts/font.georgia-helvetica.css +3 -0
- package/dist/css/fonts/font.knightlab.css +642 -0
- package/dist/css/fonts/font.lustria-lato.css +3 -0
- package/dist/css/fonts/font.medula-lato.css +3 -0
- package/dist/css/fonts/font.oldstandard.css +84 -0
- package/dist/css/fonts/font.opensans-gentiumbook.css +183 -0
- package/dist/css/fonts/font.playfair-faunaone.css +39 -0
- package/dist/css/fonts/font.playfair.css +147 -0
- package/dist/css/fonts/font.pt.css +183 -0
- package/dist/css/fonts/font.roboto-megrim.css +3 -0
- package/dist/css/fonts/font.rufina-sintony.css +3 -0
- package/dist/css/fonts/font.ubuntu.css +291 -0
- package/dist/css/fonts/font.unicaone-vollkorn.css +84 -0
- package/dist/css/storymap.css +1 -1
- package/dist/demo.html +3 -2
- package/dist/docs/api/.nojekyll +1 -0
- package/dist/docs/api/assets/custom.css +47 -0
- package/dist/docs/api/assets/hierarchy.js +1 -0
- package/dist/docs/api/assets/highlight.css +57 -0
- package/dist/docs/api/assets/icons.js +18 -0
- package/dist/docs/api/assets/icons.svg +1 -0
- package/dist/docs/api/assets/main.js +60 -0
- package/dist/docs/api/assets/navigation.js +1 -0
- package/dist/docs/api/assets/search.js +1 -0
- package/dist/docs/api/assets/style.css +1648 -0
- package/dist/docs/api/classes/StoryMap.html +2112 -0
- package/dist/docs/api/functions/MediaType.html +51 -0
- package/dist/docs/api/functions/isPresentation3Collection.html +47 -0
- package/dist/docs/api/functions/isPresentation3Manifest.html +53 -0
- package/dist/docs/api/functions/isSlideshowCollection.html +51 -0
- package/dist/docs/api/functions/loadCSS.html +63 -0
- package/dist/docs/api/functions/manifestToStorymapData.html +49 -0
- package/dist/docs/api/functions/setLanguage.html +54 -0
- package/dist/docs/api/functions/slideshowToStorymapData.html +51 -0
- package/dist/docs/api/functions/slideshowVersion.html +48 -0
- package/dist/docs/api/functions/storymapToManifest.html +57 -0
- package/dist/docs/api/functions/validateStorymap.html +53 -0
- package/dist/docs/api/functions/validateStorymapAndReport.html +54 -0
- package/dist/docs/api/hierarchy.html +28 -0
- package/dist/docs/api/index.html +300 -0
- package/dist/docs/api/interfaces/AnimationHandle.html +60 -0
- package/dist/docs/api/interfaces/ImagereadyPayload.html +71 -0
- package/dist/docs/api/interfaces/LanguageEntry.html +102 -0
- package/dist/docs/api/interfaces/LatLngLiteral.html +73 -0
- package/dist/docs/api/interfaces/LoadOptions.html +59 -0
- package/dist/docs/api/interfaces/MapEvents.html +116 -0
- package/dist/docs/api/interfaces/MapMarkerEvents.html +66 -0
- package/dist/docs/api/interfaces/MarkerEventPayload.html +57 -0
- package/dist/docs/api/interfaces/MediaEvents.html +78 -0
- package/dist/docs/api/interfaces/MenuBarEvents.html +114 -0
- package/dist/docs/api/interfaces/MessageEvents.html +72 -0
- package/dist/docs/api/interfaces/OlLayer.html +1400 -0
- package/dist/docs/api/interfaces/OlMap.html +1656 -0
- package/dist/docs/api/interfaces/OlProjection.html +332 -0
- package/dist/docs/api/interfaces/OlSource.html +784 -0
- package/dist/docs/api/interfaces/OlTileLayer.html +1497 -0
- package/dist/docs/api/interfaces/OlVectorLayer.html +1559 -0
- package/dist/docs/api/interfaces/OlView.html +1958 -0
- package/dist/docs/api/interfaces/SlideBackgroundState.html +66 -0
- package/dist/docs/api/interfaces/SlideEvents.html +78 -0
- package/dist/docs/api/interfaces/SlideNavEvents.html +72 -0
- package/dist/docs/api/interfaces/SlideshowPlayerSettings.html +182 -0
- package/dist/docs/api/interfaces/SlideshowToStorymapResult.html +60 -0
- package/dist/docs/api/interfaces/SlideshowTranslatorOptions.html +65 -0
- package/dist/docs/api/interfaces/SlideshowWarning.html +72 -0
- package/dist/docs/api/interfaces/StoryMapEvents.html +146 -0
- package/dist/docs/api/interfaces/StoryMapMarker.html +763 -0
- package/dist/docs/api/interfaces/StorySliderEvents.html +102 -0
- package/dist/docs/api/interfaces/StorymapBasemap.html +69 -0
- package/dist/docs/api/interfaces/StorymapData.html +77 -0
- package/dist/docs/api/interfaces/StorymapDataWrapper.html +60 -0
- package/dist/docs/api/interfaces/StorymapDocument.html +68 -0
- package/dist/docs/api/interfaces/StorymapError.html +63 -0
- package/dist/docs/api/interfaces/StorymapGeoreference.html +93 -0
- package/dist/docs/api/interfaces/StorymapGeoreferenceBody.html +74 -0
- package/dist/docs/api/interfaces/StorymapGroundControlPoint.html +69 -0
- package/dist/docs/api/interfaces/StorymapLanguageMap.html +57 -0
- package/dist/docs/api/interfaces/StorymapManifest.html +118 -0
- package/dist/docs/api/interfaces/StorymapManifestAnnotation.html +137 -0
- package/dist/docs/api/interfaces/StorymapManifestBackground.html +81 -0
- package/dist/docs/api/interfaces/StorymapManifestCanvas.html +188 -0
- package/dist/docs/api/interfaces/StorymapManifestContentResource.html +97 -0
- package/dist/docs/api/interfaces/StorymapManifestFeatureCollection.html +82 -0
- package/dist/docs/api/interfaces/StorymapManifestGeoreferencing.html +87 -0
- package/dist/docs/api/interfaces/StorymapManifestImageService.html +69 -0
- package/dist/docs/api/interfaces/StorymapManifestRange.html +75 -0
- package/dist/docs/api/interfaces/StorymapManifestService.html +77 -0
- package/dist/docs/api/interfaces/StorymapManifestSpecificResource.html +69 -0
- package/dist/docs/api/interfaces/StorymapManifestStatement.html +63 -0
- package/dist/docs/api/interfaces/StorymapMapOptions.html +92 -0
- package/dist/docs/api/interfaces/StorymapOptions.html +734 -0
- package/dist/docs/api/interfaces/StorymapOverlayLayer.html +157 -0
- package/dist/docs/api/interfaces/StorymapSlide.html +186 -0
- package/dist/docs/api/interfaces/StorymapSlideBackground.html +72 -0
- package/dist/docs/api/interfaces/StorymapSlideLocation.html +161 -0
- package/dist/docs/api/interfaces/StorymapSlideMarker.html +103 -0
- package/dist/docs/api/interfaces/StorymapSlideMedia.html +149 -0
- package/dist/docs/api/interfaces/StorymapSlideNarration.html +117 -0
- package/dist/docs/api/interfaces/StorymapSlideText.html +72 -0
- package/dist/docs/api/interfaces/StorymapTilejson.html +105 -0
- package/dist/docs/api/interfaces/SwipableEvents.html +102 -0
- package/dist/docs/api/interfaces/TextEvents.html +66 -0
- package/dist/docs/api/media/es.json +31 -0
- package/dist/docs/api/media/events.md +66 -0
- package/dist/docs/api/media/migration-from-knightlab.md +399 -0
- package/dist/docs/api/media/slideshow.md +109 -0
- package/dist/docs/api/media/storymap-as-iiif-manifest.md +1007 -0
- package/dist/docs/api/modules.html +50 -0
- package/dist/docs/api/types/ContentState.html +70 -0
- package/dist/docs/api/types/FiredEvent.html +43 -0
- package/dist/docs/api/types/ImagereadyKind.html +34 -0
- package/dist/docs/api/types/MediaTypeMatch.html +78 -0
- package/dist/docs/api/types/SwipeDirection.html +36 -0
- package/dist/docs/api/types/TileSourceFactory.html +53 -0
- package/dist/docs/authoring.html +306 -0
- package/dist/docs/events.html +263 -0
- package/dist/docs/iiif.html +738 -69
- package/dist/docs/migration.html +360 -11
- package/dist/docs/readme.html +149 -17
- package/dist/embed/index.html +8 -1
- package/dist/examples/font-css-false.json +33 -0
- package/dist/examples/issue-242-nonlatin.json +55 -0
- package/dist/examples/issue-305-start-at-slide-data.json +96 -0
- package/dist/examples/issue-358-iframe.json +57 -0
- package/dist/examples/issue-iiif-geo.json +32 -0
- package/dist/examples/issue-image-alt.json +23 -0
- package/dist/examples/issue-image-region.json +42 -0
- package/dist/examples/issue-image-srcset.json +24 -0
- package/dist/examples/layers-control.json +60 -0
- package/dist/examples/map-area-overlays.json +68 -0
- package/dist/examples/map-template-provider.json +36 -0
- package/dist/examples/multi-media.json +71 -0
- package/dist/examples/no-map.json +44 -0
- package/dist/examples-iiif/annotated-image.json +204 -0
- package/dist/examples-iiif/bob-boilen.json +11 -5
- package/dist/examples-iiif/courbet.json +9 -6
- package/dist/examples-iiif/empty.json +8 -4
- package/dist/examples-iiif/flickr-drewvigal.json +248 -24
- package/dist/examples-iiif/flickr-zach.wise.json +8 -4
- package/dist/examples-iiif/font-css-false.json +114 -0
- package/dist/examples-iiif/georeferenced-layer-unsupported.json +153 -0
- package/dist/examples-iiif/georeferenced-layer.json +318 -0
- package/dist/examples-iiif/iiif-wellcome.json +9 -6
- package/dist/examples-iiif/instagram-chenpamela.json +381 -36
- package/dist/examples-iiif/instagram_brentfoster.json +429 -40
- package/dist/examples-iiif/instagram_couch.json +261 -26
- package/dist/examples-iiif/instagram_joegermuska.json +441 -41
- package/dist/examples-iiif/instagram_matteich.json +261 -26
- package/dist/examples-iiif/instagram_mirandamulligan.json +261 -26
- package/dist/examples-iiif/instagram_raymondmccrea.json +261 -26
- package/dist/examples-iiif/instagram_test.json +261 -26
- package/dist/examples-iiif/issue-134-no-lines.json +9 -5
- package/dist/examples-iiif/issue-177-color-vars.json +9 -5
- package/dist/examples-iiif/issue-211-rtl.json +9 -5
- package/dist/examples-iiif/issue-242-nonlatin.json +239 -0
- package/dist/examples-iiif/issue-272-facebook.json +9 -5
- package/dist/examples-iiif/issue-305-start-at-slide-data.json +305 -0
- package/dist/examples-iiif/issue-305-start-at-slide.json +9 -5
- package/dist/examples-iiif/issue-341-distance.json +9 -5
- package/dist/examples-iiif/issue-358-iframe.json +190 -0
- package/dist/examples-iiif/issue-360-juxtapose.json +22 -7
- package/dist/examples-iiif/issue-368-null-fields.json +9 -5
- package/dist/examples-iiif/issue-375-code-lists.json +9 -5
- package/dist/examples-iiif/issue-381-dateline.json +9 -5
- package/dist/examples-iiif/issue-405-custom-icon.json +203 -0
- package/dist/examples-iiif/issue-425-many-slides.json +9 -5
- package/dist/examples-iiif/issue-428-background.json +20 -8
- package/dist/examples-iiif/issue-435-googlemaps.json +9 -5
- package/dist/examples-iiif/issue-437-documentcloud.json +22 -7
- package/dist/examples-iiif/issue-451-webp.json +9 -5
- package/dist/examples-iiif/issue-455-audio.json +9 -5
- package/dist/examples-iiif/issue-480-shorts.json +9 -5
- package/dist/examples-iiif/issue-506-marker-sync.json +9 -5
- package/dist/examples-iiif/issue-iiif-geo.json +200 -0
- package/dist/examples-iiif/issue-image-alt.json +90 -0
- package/dist/examples-iiif/issue-image-region.json +203 -0
- package/dist/examples-iiif/issue-image-srcset.json +87 -0
- package/dist/examples-iiif/jansteen.json +9 -6
- package/dist/examples-iiif/katrina.json +197 -41
- package/dist/examples-iiif/layers-control.json +226 -0
- package/dist/examples-iiif/map-area-overlays.json +289 -0
- package/dist/examples-iiif/map-template-provider.json +133 -0
- package/dist/examples-iiif/marktwain.json +255 -58
- package/dist/examples-iiif/multi-media.json +237 -0
- package/dist/examples-iiif/no-map.json +148 -0
- package/dist/examples-iiif/population.json +440 -76
- package/dist/examples-iiif/president.json +456 -83
- package/dist/examples-iiif/scroll-hint.json +190 -0
- package/dist/examples-iiif/seurat.json +113 -22
- package/dist/examples-iiif/zoomify-clamp.json +153 -0
- package/dist/examples-slideshow/single.json +79 -0
- package/dist/examples-slideshow/static/pixel.png +0 -0
- package/dist/examples-slideshow/static.json +44 -0
- package/dist/harness-multi.html +17 -0
- package/dist/harness.html +4 -2
- package/dist/index.html +19 -12
- package/dist/js/storymap.d.ts +2669 -307
- package/dist/js/storymap.js +10820 -6196
- package/dist/js/storymap.js.map +1 -1
- package/dist/navplace-properties.json +54 -0
- package/dist/site.css +54 -36
- package/docs/migration-from-knightlab.md +279 -19
- package/docs/storymap-as-iiif-manifest.md +633 -136
- package/package.json +12 -7
- package/schema/storymap.schema.json +733 -80
- package/src/animation/easings.ts +8 -1
- package/src/animation/tween.ts +186 -0
- package/src/core/Browser.ts +6 -5
- package/src/core/Load.ts +54 -2
- package/src/core/Util.ts +257 -193
- package/src/core/mixins.ts +66 -35
- package/src/core/viewers.ts +94 -0
- package/src/dom/DomEvent.ts +27 -8
- package/src/globals.d.ts +8 -0
- package/src/language/Language.ts +206 -25
- package/src/language/locale/.expected-gaps.json +346 -0
- package/src/language/locale/be.json +15 -2
- package/src/language/locale/bg.json +15 -2
- package/src/language/locale/cs.json +15 -2
- package/src/language/locale/de.json +23 -2
- package/src/language/locale/el.json +15 -2
- package/src/language/locale/en.json +17 -3
- package/src/language/locale/es.json +15 -2
- package/src/language/locale/et.json +5 -1
- package/src/language/locale/fr.json +15 -2
- package/src/language/locale/he.json +16 -3
- package/src/language/locale/hu.json +15 -2
- package/src/language/locale/is.json +5 -1
- package/src/language/locale/it.json +15 -2
- package/src/language/locale/jp.json +15 -2
- package/src/language/locale/ko.json +15 -2
- package/src/language/locale/nl.json +15 -2
- package/src/language/locale/nn.json +5 -1
- package/src/language/locale/no.json +15 -2
- package/src/language/locale/pl.json +15 -2
- package/src/language/locale/pt.json +15 -2
- package/src/language/locale/ru.json +15 -2
- package/src/language/locale/sk.json +15 -2
- package/src/language/locale/sr.json +5 -1
- package/src/language/locale/sv.json +15 -2
- package/src/language/locale/tr.json +15 -2
- package/src/language/locale/uk.json +15 -2
- package/src/language/locale/ur.json +5 -1
- package/src/language/locale/zh-cn.json +15 -2
- package/src/language/locale/zh-tw.json +15 -2
- package/src/main.ts +143 -0
- package/src/map/Map.ts +227 -112
- package/src/map/MapMarker.ts +44 -8
- package/src/map/georeference.ts +325 -0
- package/src/map/openlayers/Map.OpenLayers.ts +1601 -263
- package/src/map/openlayers/MapMarker.OpenLayers.ts +348 -24
- package/src/map/openlayers/zoomifyTiles.ts +1 -4
- package/src/map/slideView.ts +149 -0
- package/src/map/types.ts +2 -0
- package/src/media/EmbedUtil.ts +333 -24
- package/src/media/Media.ts +270 -59
- package/src/media/embedId.ts +110 -0
- package/src/media/types/Audio.ts +9 -82
- package/src/media/types/Blockquote.ts +2 -3
- package/src/media/types/DailyMotion.ts +5 -10
- package/src/media/types/DocumentCloud.ts +6 -35
- package/src/media/types/Facebook.ts +3 -4
- package/src/media/types/Flickr.ts +23 -12
- package/src/media/types/GoogleDoc.ts +3 -4
- package/src/media/types/HtmlMedia.ts +233 -0
- package/src/media/types/IFrame.ts +3 -4
- package/src/media/types/Image.ts +87 -9
- package/src/media/types/Juxtapose.ts +14 -5
- package/src/media/types/SoundCloud.ts +28 -6
- package/src/media/types/Text.ts +46 -21
- package/src/media/types/Twitter.ts +35 -20
- package/src/media/types/Video.ts +9 -79
- package/src/media/types/Vimeo.ts +11 -11
- package/src/media/types/Website.ts +23 -13
- package/src/media/types/Wikipedia.ts +18 -8
- package/src/media/types/YouTube.ts +72 -40
- package/src/scss/Typography.scss +16 -18
- package/src/scss/VCO.StoryMap.scss +208 -12
- package/src/scss/Variables.scss +3 -2
- package/src/scss/core/Mixins.scss +5 -63
- package/src/scss/core/Reset.scss +5 -18
- package/src/scss/fonts/font.amatic-andika.scss +3 -0
- package/src/scss/fonts/font.bitter-raleway.scss +6 -0
- package/src/scss/fonts/font.cairo.scss +79 -0
- package/src/scss/fonts/font.clicker-garamond.scss +5 -0
- package/src/scss/fonts/font.dancing-ledger.scss +1 -0
- package/src/scss/fonts/font.knightlab.scss +14 -0
- package/src/scss/fonts/font.oldstandard.scss +6 -0
- package/src/scss/fonts/font.opensans-gentiumbook.scss +5 -0
- package/src/scss/fonts/font.playfair-faunaone.scss +2 -0
- package/src/scss/fonts/font.playfair.scss +4 -0
- package/src/scss/fonts/font.pt.scss +6 -0
- package/src/scss/fonts/font.ubuntu.scss +4 -0
- package/src/scss/fonts/font.unicaone-vollkorn.scss +5 -0
- package/src/scss/map/VCO.Map.scss +1 -1
- package/src/scss/map/VCO.MapMarker.scss +114 -8
- package/src/scss/map/openlayers/VCO.Map.OpenLayers.scss +7 -4
- package/src/scss/media/VCO.Media.scss +14 -17
- package/src/scss/media/types/VCO.Media.Blockquote.scss +1 -1
- package/src/scss/media/types/VCO.Media.Text.scss +13 -5
- package/src/scss/media/types/VCO.Media.Twitter.scss +4 -4
- package/src/scss/media/types/VCO.Media.Wikipedia.scss +3 -19
- package/src/scss/site/site.scss +55 -36
- package/src/scss/slider/VCO.Slide.scss +61 -13
- package/src/scss/slider/VCO.SlideNav.scss +34 -24
- package/src/scss/slider/VCO.StorySlider.scss +53 -1
- package/src/scss/snapmap/VCO.SnapMap.scss +3 -3
- package/src/scss/ui/VCO.Consent.scss +63 -13
- package/src/scss/ui/VCO.Layers.scss +119 -0
- package/src/scss/ui/VCO.MenuBar.Button.scss +143 -14
- package/src/scss/ui/VCO.MenuBar.scss +9 -2
- package/src/scss/ui/VCO.Message.scss +16 -5
- package/src/site/site.ts +2 -1
- package/src/slider/Slide.ts +326 -74
- package/src/slider/SlideNav.ts +54 -9
- package/src/slider/StorySlider.ts +352 -60
- package/src/storymap/Consent.ts +429 -82
- package/src/storymap/StoryMap.ts +2163 -340
- package/src/storymap/content-state.ts +180 -0
- package/src/storymap/font-css.ts +72 -0
- package/src/storymap/from-slideshow.ts +785 -0
- package/src/storymap/iiif-shared.ts +228 -0
- package/src/storymap/iiif.ts +1742 -95
- package/src/storymap/to-iiif.ts +1008 -0
- package/src/storymap/validate.ts +80 -7
- package/src/types.ts +591 -29
- package/src/ui/LayersControl.ts +302 -0
- package/src/ui/MenuBar.ts +325 -61
- package/src/ui/Message.ts +44 -2
- package/src/ui/Swipable.ts +170 -85
- package/dist/assets/demo-D6cTmzn6.js +0 -1
- package/dist/assets/harness-BPFlzutp.js +0 -1
- package/dist/assets/index-DDsPBPZA.js +0 -1
- package/dist/assets/main-Y-MYUSJ4.js +0 -17
- package/src/animation/morpheus.d.ts +0 -41
- package/src/scss/VCO.StoryMap.Dark.scss +0 -138
- package/src/scss/Variables.Dark.scss +0 -56
- package/src/scss/media/types/VCO.Media.Instagram.scss +0 -7
- package/src/scss/media/types/VCO.Media.Profile.scss +0 -5
- package/src/ui/Draggable.ts +0 -374
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
import OlMap from "ol/Map";
|
|
2
2
|
import View from "ol/View";
|
|
3
|
+
import { unByKey } from "ol/Observable";
|
|
3
4
|
import type { Control } from "ol/control";
|
|
4
5
|
import type { Interaction } from "ol/interaction";
|
|
5
|
-
import { Tile as TileLayer, Vector as VectorLayer } from "ol/layer";
|
|
6
|
+
import { Image as ImageLayer, Tile as TileLayer, Vector as VectorLayer } from "ol/layer";
|
|
7
|
+
import Layer from "ol/layer/Layer";
|
|
6
8
|
import VectorTileLayer from "ol/layer/VectorTile";
|
|
7
9
|
import { XYZ, OSM, IIIF } from "ol/source";
|
|
10
|
+
import Static from "ol/source/ImageStatic";
|
|
11
|
+
import type Source from "ol/source/Source";
|
|
8
12
|
import VectorSource from "ol/source/Vector";
|
|
9
13
|
import LineString from "ol/geom/LineString";
|
|
10
14
|
import Feature from "ol/Feature";
|
|
@@ -25,9 +29,33 @@ import "ol/ol.css";
|
|
|
25
29
|
import Map from "../Map";
|
|
26
30
|
import OpenLayersMapMarker from "./MapMarker.OpenLayers";
|
|
27
31
|
import { padCroppedZoomifyTile } from "./zoomifyTiles";
|
|
32
|
+
import { prefersReducedMotion } from "../../core/Util";
|
|
28
33
|
import type { LinePoint, ViewToOptions } from "../types";
|
|
29
|
-
import type {
|
|
30
|
-
|
|
34
|
+
import type {
|
|
35
|
+
LatLngLiteral,
|
|
36
|
+
StorymapImageOverlay,
|
|
37
|
+
StorymapOverlayLayer,
|
|
38
|
+
StorymapTilejson,
|
|
39
|
+
StorymapSlide,
|
|
40
|
+
StorymapSlideLocation,
|
|
41
|
+
} from "../../types";
|
|
42
|
+
import { fitGeoreference, resolveInfoJsonUrl } from "../georeference";
|
|
43
|
+
import {
|
|
44
|
+
buildSlideFilter,
|
|
45
|
+
isStaticImageUrl,
|
|
46
|
+
normalizeMask,
|
|
47
|
+
serviceToInfoJson,
|
|
48
|
+
slideBasemapKey,
|
|
49
|
+
slideRotationRad,
|
|
50
|
+
type NormalizedMask,
|
|
51
|
+
} from "../slideView";
|
|
52
|
+
import {
|
|
53
|
+
consentManagerOf,
|
|
54
|
+
tileService,
|
|
55
|
+
type ConsentManager,
|
|
56
|
+
type ConsentService,
|
|
57
|
+
} from "../../storymap/Consent";
|
|
58
|
+
import { sanitizeSlideText } from "../../media/EmbedUtil";
|
|
31
59
|
|
|
32
60
|
/* Map.OpenLayers
|
|
33
61
|
Creates a Map using OpenLayers
|
|
@@ -35,6 +63,30 @@ import { consentManagerOf, consentMessage, type ConsentManager } from "../../sto
|
|
|
35
63
|
|
|
36
64
|
const MAX_ZOOM = 19;
|
|
37
65
|
|
|
66
|
+
/**
|
|
67
|
+
* Natural-size probe timeout for the static fallback: how long the viewer
|
|
68
|
+
* waits for the fallback image's dimensions before giving up and logging
|
|
69
|
+
* the IIIF error. A named constant (rather than an inline literal) so hosts
|
|
70
|
+
* and tests can find the single knob. There is no shared network-timeout
|
|
71
|
+
* option to reuse — `options.duration` is animation timing and the 1200ms
|
|
72
|
+
* media delay is a load deferral, not a watchdog.
|
|
73
|
+
*/
|
|
74
|
+
const STATIC_PROBE_TIMEOUT_MS = 15000;
|
|
75
|
+
|
|
76
|
+
/** What kind of imagery an `imageready` source carries. */
|
|
77
|
+
export type ImagereadyKind = "iiif" | "zoomify" | "tiles";
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Payload of the `imageready` event: the source that became usable, what
|
|
81
|
+
* kind of imagery it is, and the layer carrying it (`null` when the source
|
|
82
|
+
* outlives its layer).
|
|
83
|
+
*/
|
|
84
|
+
export interface ImagereadyPayload {
|
|
85
|
+
source: { getState?(): string };
|
|
86
|
+
kind: ImagereadyKind;
|
|
87
|
+
layer: Layer | null;
|
|
88
|
+
}
|
|
89
|
+
|
|
38
90
|
/**
|
|
39
91
|
* Zoom ladder for image-space maps (issue #465): rung 0 shows any image fully
|
|
40
92
|
* zoomed out, finer rungs reach sub-pixel detail. It keeps view zooms,
|
|
@@ -45,24 +97,73 @@ const IMAGE_RESOLUTIONS = Array.from({ length: 25 }, (_, i) => 2 ** (16 - i));
|
|
|
45
97
|
|
|
46
98
|
export default class OpenLayers extends Map {
|
|
47
99
|
declare "_map": OlMap;
|
|
48
|
-
declare "_tile_layer":
|
|
100
|
+
declare "_tile_layer": Layer;
|
|
49
101
|
declare "_line": VectorLayer;
|
|
50
102
|
declare "_line_active": VectorLayer;
|
|
51
|
-
declare "_tile_layer_mini":
|
|
103
|
+
declare "_tile_layer_mini": Layer | null;
|
|
52
104
|
declare "_mini_map": OverviewMap;
|
|
53
105
|
declare "_markers": OpenLayersMapMarker[];
|
|
54
106
|
/** App-level stacked overlays (see the `overlays` option) */
|
|
55
|
-
declare "_overlay_layers":
|
|
107
|
+
declare "_overlay_layers": Layer[];
|
|
108
|
+
/**
|
|
109
|
+
* The `overlays[]` entries that produced a layer, parallel to
|
|
110
|
+
* `_overlay_layers`: a malformed entry is skipped, so the two arrays
|
|
111
|
+
* can be shorter than the option list.
|
|
112
|
+
*/
|
|
113
|
+
declare "_overlay_entries": StorymapOverlayLayer[];
|
|
56
114
|
/** rAF handle of the running active-line draw animation */
|
|
57
115
|
declare "_line_animation": number | null;
|
|
116
|
+
/** Sources `imageready` has already been fired for. */
|
|
117
|
+
declare "_imageready_fired": WeakSet<object>;
|
|
118
|
+
/** The `loadend` subscription key, stored so dispose() can detach it. */
|
|
119
|
+
declare "_loadend_key": Parameters<typeof unByKey>[0] | null;
|
|
120
|
+
/**
|
|
121
|
+
* Per-slide basemap layers by key (multi-manifest tours): built on first
|
|
122
|
+
* use through the tile layer factory, then reused. Empty until a slide
|
|
123
|
+
* names a `location.basemap`.
|
|
124
|
+
*/
|
|
125
|
+
declare "_basemap_cache": globalThis.Map<string, Layer>;
|
|
126
|
+
/**
|
|
127
|
+
* The active per-slide basemap key, or null when the story basemap is
|
|
128
|
+
* showing. Restored to the story basemap on slides without one.
|
|
129
|
+
*/
|
|
130
|
+
declare "_slide_basemap": string | null;
|
|
131
|
+
/** Spotlight mask element (slideshow passepartout), or null when off. */
|
|
132
|
+
declare "_mask_el": HTMLDivElement | null;
|
|
133
|
+
/** Per-slide image overlay layer (slideshow imgoverlay), or null. */
|
|
134
|
+
declare "_imgoverlay_layer": Layer | null;
|
|
135
|
+
/** Guards stale static-fallback probes (see `_probeStaticFallback`). */
|
|
136
|
+
declare "_static_probe_token": number;
|
|
137
|
+
/**
|
|
138
|
+
* Set by `dispose()` so in-flight fetches and probes resolve into
|
|
139
|
+
* no-ops instead of touching a torn-down map.
|
|
140
|
+
*/
|
|
141
|
+
declare "_disposed": boolean;
|
|
142
|
+
/**
|
|
143
|
+
* Per-URL probe tokens for static slide basemaps: concurrent probes for
|
|
144
|
+
* different slides must not cancel each other (see
|
|
145
|
+
* `_createStaticImageBasemap`).
|
|
146
|
+
*/
|
|
147
|
+
declare "_slide_probe_tokens": globalThis.Map<string, number>;
|
|
58
148
|
|
|
59
149
|
/* Create the Map
|
|
60
150
|
================================================== */
|
|
61
151
|
_createMap(): void {
|
|
62
|
-
const is_image_map = this.
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
152
|
+
const is_image_map = this.isImageSpace();
|
|
153
|
+
this._imageready_fired = new WeakSet();
|
|
154
|
+
this._disposed = false;
|
|
155
|
+
this._basemap_cache = new globalThis.Map();
|
|
156
|
+
this._slide_basemap = null;
|
|
157
|
+
this._story_basemap_layer = null;
|
|
158
|
+
this._mask_el = null;
|
|
159
|
+
this._imgoverlay_layer = null;
|
|
160
|
+
this._static_probe_token = 0;
|
|
161
|
+
this._slide_probe_tokens = new globalThis.Map();
|
|
162
|
+
|
|
163
|
+
// Caller-supplied OpenLayers options: `controls` replaces the defaults
|
|
164
|
+
// (the viewer installs none), `interactions` are added to the viewer's
|
|
165
|
+
// own pan/zoom set, `view` merges over the computed default, everything
|
|
166
|
+
// else passes through
|
|
66
167
|
const user_map_options = this.options.map_options ?? {};
|
|
67
168
|
const { element: _element, view: user_view, ...passthrough } = user_map_options;
|
|
68
169
|
const user_view_options = (user_view ?? {}) as Record<string, unknown>;
|
|
@@ -76,6 +177,16 @@ export default class OpenLayers extends Map {
|
|
|
76
177
|
// space maps use raw pixel coordinates.
|
|
77
178
|
const bbox_extent = this._bboxExtent();
|
|
78
179
|
|
|
180
|
+
// A TileJSON tile source states its own zoom ladder, its covered area
|
|
181
|
+
// and its default view, and all three are honoured here (§2.9). A
|
|
182
|
+
// keyword basemap has none of these, so the defaults stand.
|
|
183
|
+
const tilejson = this._tilejsonFor(this.options.map_type);
|
|
184
|
+
const tile_min_zoom = tilejson?.minzoom ?? 0;
|
|
185
|
+
const tile_max_zoom = tilejson?.maxzoom ?? MAX_ZOOM;
|
|
186
|
+
const tile_extent = tilejson?.bounds
|
|
187
|
+
? fromLonLat(tilejson.bounds as [number, number, number, number])
|
|
188
|
+
: null;
|
|
189
|
+
|
|
79
190
|
this._map = new OlMap({
|
|
80
191
|
...passthrough,
|
|
81
192
|
target: this._el.map,
|
|
@@ -83,10 +194,19 @@ export default class OpenLayers extends Map {
|
|
|
83
194
|
interactions: (user_map_options.interactions as Interaction[]) ?? [],
|
|
84
195
|
view: new View({
|
|
85
196
|
projection: is_image_map ? "EPSG:4326" : "EPSG:3857",
|
|
86
|
-
center
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
197
|
+
...(tilejson?.center
|
|
198
|
+
? {
|
|
199
|
+
center: fromLonLat([tilejson.center[0], tilejson.center[1]]),
|
|
200
|
+
zoom: tilejson.center[2],
|
|
201
|
+
}
|
|
202
|
+
: { center: [0, 0], zoom: 0 }),
|
|
203
|
+
minZoom: tile_min_zoom,
|
|
204
|
+
maxZoom: is_image_map ? IMAGE_RESOLUTIONS.length - 1 : tile_max_zoom,
|
|
205
|
+
// a tile source that only covers part of the world: keep the
|
|
206
|
+
// view inside what it actually has tiles for
|
|
207
|
+
...(tile_extent && !is_image_map
|
|
208
|
+
? { extent: tile_extent, constrainOnlyCenter: true }
|
|
209
|
+
: {}),
|
|
90
210
|
// image maps live outside the 4326 world: opt out of the
|
|
91
211
|
// global-projection constraints (they cap resolution at
|
|
92
212
|
// fit-the-world and clamp the center to [-90, 90]) and use
|
|
@@ -102,7 +222,7 @@ export default class OpenLayers extends Map {
|
|
|
102
222
|
}),
|
|
103
223
|
});
|
|
104
224
|
|
|
105
|
-
this._map.on("loadend", () => {
|
|
225
|
+
this._loadend_key = this._map.on("loadend", () => {
|
|
106
226
|
this._onMapLoaded(undefined);
|
|
107
227
|
});
|
|
108
228
|
|
|
@@ -119,11 +239,11 @@ export default class OpenLayers extends Map {
|
|
|
119
239
|
// Tile Layer — GDPR consent mode defers it until the visitor allows
|
|
120
240
|
// map tiles (the consent bar renders over the map)
|
|
121
241
|
const consent = consentManagerOf(this.options);
|
|
122
|
-
const tile_service =
|
|
242
|
+
const tile_service = tileService();
|
|
123
243
|
if (this.options.consent_required && consent) {
|
|
124
|
-
if (consent.isGranted(tile_service)) {
|
|
244
|
+
if (consent.isGranted(tile_service.key)) {
|
|
125
245
|
this._addTileLayer();
|
|
126
|
-
} else if (!consent.isDenied(tile_service)) {
|
|
246
|
+
} else if (!consent.isDenied(tile_service.key)) {
|
|
127
247
|
void this._requestTileConsent(consent, tile_service);
|
|
128
248
|
}
|
|
129
249
|
// denied → the map renders with background color and markers only
|
|
@@ -131,7 +251,8 @@ export default class OpenLayers extends Map {
|
|
|
131
251
|
this._addTileLayer();
|
|
132
252
|
}
|
|
133
253
|
|
|
134
|
-
// Create Overall Connection Line
|
|
254
|
+
// Create Overall Connection Line (reserved z-range 10/11 for the
|
|
255
|
+
// route lines; see the stack-order note at _addTileLayer)
|
|
135
256
|
this._line = this._createLine();
|
|
136
257
|
this._line.setStyle(this._lineStyle(this.options.line_color_inactive));
|
|
137
258
|
this._addLineToMap(this._line);
|
|
@@ -152,6 +273,7 @@ export default class OpenLayers extends Map {
|
|
|
152
273
|
|
|
153
274
|
// Stacked raster overlays (base tiles 1.., below the route lines)
|
|
154
275
|
this._overlay_layers = [];
|
|
276
|
+
this._overlay_entries = [];
|
|
155
277
|
this._buildOverlays();
|
|
156
278
|
|
|
157
279
|
// Native interactions (pan/zoom), no scroll zoom by default
|
|
@@ -174,6 +296,57 @@ export default class OpenLayers extends Map {
|
|
|
174
296
|
this._updateAttribution();
|
|
175
297
|
}
|
|
176
298
|
|
|
299
|
+
/* Public accessors
|
|
300
|
+
Consumers get the raw OpenLayers map via `storymap.map`; these hand out
|
|
301
|
+
the internal objects the viewer keeps private (base/overlay/minimap layers,
|
|
302
|
+
the route lines, the markers) so nothing has to be reached for through an
|
|
303
|
+
underscore field. Layer order in `map.getLayers()` is NOT the overlay
|
|
304
|
+
order (creation order is [base, line, line_active, overlay0…] and zIndex is
|
|
305
|
+
a separate axis), so `getOverlayLayers()` is the only correct way to map an
|
|
306
|
+
`overlays[]` index onto a layer.
|
|
307
|
+
================================================= */
|
|
308
|
+
|
|
309
|
+
/** The base tile layer (the one created for `map_type`), if consent has not deferred it. */
|
|
310
|
+
getBaseLayer(): Layer | null {
|
|
311
|
+
return this._tile_layer ?? null;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/** The stacked `overlays[]` layers, in `overlays[]` order. */
|
|
315
|
+
getOverlayLayers(): Layer[] {
|
|
316
|
+
return [...this._overlay_layers];
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/** One stacked overlay layer by its `overlays[]` index, or `null`. */
|
|
320
|
+
getOverlayLayer(index: number): Layer | null {
|
|
321
|
+
return this._overlay_layers[index] ?? null;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/** The minimap's OpenLayers map (the `OverviewMap` control's inner map). */
|
|
325
|
+
getMinimap(): OlMap | null {
|
|
326
|
+
const overview = this._mini_map?.getOverviewMap?.() ?? null;
|
|
327
|
+
return overview ?? null;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/** The full (inactive) route line layer. */
|
|
331
|
+
getLine(): Layer | null {
|
|
332
|
+
return this._line ?? null;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/** The highlighted route line layer drawn up to the current slide. */
|
|
336
|
+
getLineActive(): Layer | null {
|
|
337
|
+
return this._line_active ?? null;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/** The markers, in slide order (the index matches the slide index). */
|
|
341
|
+
getMarkers(): OpenLayersMapMarker[] {
|
|
342
|
+
return [...this._markers];
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/** One marker by slide index, or `null`. */
|
|
346
|
+
getMarker(index: number): OpenLayersMapMarker | null {
|
|
347
|
+
return this._markers[index] ?? null;
|
|
348
|
+
}
|
|
349
|
+
|
|
177
350
|
/**
|
|
178
351
|
* (Re)render the attribution line for the current map type. Called at
|
|
179
352
|
* creation and on every `map_type` switch, which previously left the
|
|
@@ -181,32 +354,171 @@ export default class OpenLayers extends Map {
|
|
|
181
354
|
*/
|
|
182
355
|
_updateAttribution(): void {
|
|
183
356
|
const extras = this._extra_attributions ?? [];
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
357
|
+
// Identical credits (e.g. stacked overlays sharing one source, or the
|
|
358
|
+
// attribution option restating the base credit) collapse to one entry
|
|
359
|
+
const parts = [
|
|
360
|
+
...new Set([...this._getAttribution(this.options.map_type), ...extras].filter(Boolean)),
|
|
361
|
+
].join(" | ");
|
|
187
362
|
let el = this._el.map.querySelector(".vco-map-attribution") as HTMLElement | null;
|
|
188
363
|
if (!el) {
|
|
189
364
|
this._el.map.insertAdjacentHTML("beforeend", `<div class="vco-map-attribution"></div>`);
|
|
190
365
|
el = this._el.map.querySelector(".vco-map-attribution") as HTMLElement | null;
|
|
191
366
|
}
|
|
192
367
|
if (el) {
|
|
193
|
-
|
|
368
|
+
// The library-authored credits are trusted markup, but
|
|
369
|
+
// `options.attribution` and overlay credits come from the
|
|
370
|
+
// storymap JSON. Rather than trusting them, the whole line is
|
|
371
|
+
// re-sanitized: a fragment that is not well-formed markup is
|
|
372
|
+
// unwrapped to text, so an author-supplied credit can never
|
|
373
|
+
// inject an element into the map.
|
|
374
|
+
el.textContent = "";
|
|
375
|
+
el.appendChild(sanitizeSlideText(parts));
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* Provider credit for a `map_type`, in both renderings: the linked HTML
|
|
381
|
+
* for our own `.vco-map-attribution` line and the plain text for the
|
|
382
|
+
* OpenLayers source `attributions`. One table — the two previous
|
|
383
|
+
* functions repeated the same `osm/stadia/mapbox/esri` switch and could
|
|
384
|
+
* drift apart, leaving a provider credited in one place but not the other
|
|
385
|
+
* (which their terms of service require).
|
|
386
|
+
*/
|
|
387
|
+
_providerCredit(map_type: string): { html: string; text: string } {
|
|
388
|
+
// A full XYZ template naming a known provider
|
|
389
|
+
// (https://tile.openstreetmap.org/{z}/{x}/{y}.png) is not the `osm`
|
|
390
|
+
// map type, so match its host as well as the type prefix — otherwise
|
|
391
|
+
// both renderings fall through to the generic "Map data" line.
|
|
392
|
+
const byHost = this._hostCredit(map_type);
|
|
393
|
+
if (byHost) return byHost;
|
|
394
|
+
// Emitting provider credit only for OSM left Stadia, Mapbox and the
|
|
395
|
+
// OL XYZ template providers uncredited, which their terms of service
|
|
396
|
+
// require. The credit is keyed off the resolved map_type, so a custom
|
|
397
|
+
// `{z}` template gets the generic "map data" line and the author can
|
|
398
|
+
// still override it with the `attribution` option.
|
|
399
|
+
const table: { test: (t: string) => boolean; html: string; text: string }[] = [
|
|
400
|
+
{
|
|
401
|
+
test: (t) => t === "" || t.startsWith("osm"),
|
|
402
|
+
html: "© <a target='_blank' href='https://www.openstreetmap.org/copyright'>OpenStreetMap</a> contributors",
|
|
403
|
+
text: "© OpenStreetMap contributors",
|
|
404
|
+
},
|
|
405
|
+
{
|
|
406
|
+
test: (t) => t.startsWith("stadia") || t === "stamen",
|
|
407
|
+
html:
|
|
408
|
+
'© <a target="_blank" rel="noopener noreferrer" href="https://stadiamaps.com/">Stadia Maps</a>, ' +
|
|
409
|
+
'© <a target="_blank" rel="noopener noreferrer" href="https://openmaptiles.org/">OpenMapTiles</a> ' +
|
|
410
|
+
'© <a target="_blank" rel="noopener noreferrer" href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
|
|
411
|
+
text: "© Stadia Maps, © OpenMapTiles © OpenStreetMap contributors",
|
|
412
|
+
},
|
|
413
|
+
{
|
|
414
|
+
test: (t) => t.startsWith("mapbox://"),
|
|
415
|
+
html: '© <a target="_blank" href="https://www.mapbox.com/about/maps/">Mapbox</a>',
|
|
416
|
+
text: "© Mapbox",
|
|
417
|
+
},
|
|
418
|
+
{
|
|
419
|
+
test: (t) => t.startsWith("ch-") || t.startsWith("esri"),
|
|
420
|
+
html: 'Map data © <a target="_blank" href="https://www.esri.com/">Esri</a>',
|
|
421
|
+
text: "Map data © Esri",
|
|
422
|
+
},
|
|
423
|
+
{
|
|
424
|
+
test: () => true,
|
|
425
|
+
html: "Map data",
|
|
426
|
+
text: "Map data",
|
|
427
|
+
},
|
|
428
|
+
];
|
|
429
|
+
const entry = table.find((row) => row.test(map_type)) ?? table[table.length - 1];
|
|
430
|
+
return { html: entry.html, text: entry.text };
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* The table row for an absolute tile template's host, or null when the
|
|
435
|
+
* template is relative (a host serving its own tiles credits itself via
|
|
436
|
+
* `attribution`) or unknown. Subdomains match their parent, so
|
|
437
|
+
* `a.tile.openstreetmap.org` credits OSM.
|
|
438
|
+
*/
|
|
439
|
+
_hostCredit(map_type: string): { html: string; text: string } | null {
|
|
440
|
+
let host: string | null = null;
|
|
441
|
+
try {
|
|
442
|
+
host = new URL(map_type).hostname;
|
|
443
|
+
} catch {
|
|
444
|
+
// Not absolute: only a protocol-relative template (`//host/...`)
|
|
445
|
+
// still names a host. A relative template resolves against the
|
|
446
|
+
// dummy base below and is rejected by the host check.
|
|
447
|
+
try {
|
|
448
|
+
const resolved = new URL(map_type, "https://storymap.invalid").hostname;
|
|
449
|
+
host = resolved === "storymap.invalid" ? null : resolved;
|
|
450
|
+
} catch {
|
|
451
|
+
host = null;
|
|
452
|
+
}
|
|
194
453
|
}
|
|
454
|
+
if (!host) return null;
|
|
455
|
+
const rows: { hosts: string[]; html: string; text: string }[] = [
|
|
456
|
+
{
|
|
457
|
+
hosts: ["openstreetmap.org", "osm.org"],
|
|
458
|
+
html: "© <a target='_blank' href='https://www.openstreetmap.org/copyright'>OpenStreetMap</a> contributors",
|
|
459
|
+
text: "© OpenStreetMap contributors",
|
|
460
|
+
},
|
|
461
|
+
{
|
|
462
|
+
hosts: ["stadiamaps.com", "openmaptiles.org"],
|
|
463
|
+
html:
|
|
464
|
+
'© <a target="_blank" rel="noopener noreferrer" href="https://stadiamaps.com/">Stadia Maps</a>, ' +
|
|
465
|
+
'© <a target="_blank" rel="noopener noreferrer" href="https://openmaptiles.org/">OpenMapTiles</a> ' +
|
|
466
|
+
'© <a target="_blank" rel="noopener noreferrer" href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
|
|
467
|
+
text: "© Stadia Maps, © OpenMapTiles © OpenStreetMap contributors",
|
|
468
|
+
},
|
|
469
|
+
{
|
|
470
|
+
hosts: ["mapbox.com"],
|
|
471
|
+
html: '© <a target="_blank" href="https://www.mapbox.com/about/maps/">Mapbox</a>',
|
|
472
|
+
text: "© Mapbox",
|
|
473
|
+
},
|
|
474
|
+
{
|
|
475
|
+
hosts: ["arcgisonline.com", "esri.com"],
|
|
476
|
+
html: 'Map data © <a target="_blank" href="https://www.esri.com/">Esri</a>',
|
|
477
|
+
text: "Map data © Esri",
|
|
478
|
+
},
|
|
479
|
+
{
|
|
480
|
+
hosts: ["carto.com", "basemaps.cartocdn.com"],
|
|
481
|
+
html:
|
|
482
|
+
"© <a target='_blank' href='https://www.openstreetmap.org/copyright'>OpenStreetMap</a> contributors © " +
|
|
483
|
+
'<a target="_blank" href="https://carto.com/">CARTO</a>',
|
|
484
|
+
text: "© OpenStreetMap contributors © CARTO",
|
|
485
|
+
},
|
|
486
|
+
];
|
|
487
|
+
const row = rows.find(({ hosts }) =>
|
|
488
|
+
hosts.some((h) => host === h || host.endsWith(`.${h}`)),
|
|
489
|
+
);
|
|
490
|
+
return row ? { html: row.html, text: row.text } : null;
|
|
195
491
|
}
|
|
196
492
|
|
|
197
|
-
_getAttribution(map_type: string): string {
|
|
493
|
+
_getAttribution(map_type: string): string[] {
|
|
198
494
|
const parts = [
|
|
199
495
|
"<a href='https://storymap.knightlab.com/' target='_blank' class='vco-knightlab-brand'><span>■</span> StoryMapJS</a>",
|
|
200
496
|
];
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
497
|
+
// Our own `.vco-map-attribution` line is the rendered default (the
|
|
498
|
+
// OL attribution control is not added), so this hand-built line is
|
|
499
|
+
// what the visitor sees. Sources additionally carry the plain-text
|
|
500
|
+
// credit from `_sourceAttributions`, so `source.getAttributions()`
|
|
501
|
+
// and a consumer's own Attribution control are not left empty.
|
|
502
|
+
parts.push(this._providerCredit(map_type).html);
|
|
503
|
+
if (this.options.attribution) {
|
|
504
|
+
parts.push(this.options.attribution);
|
|
205
505
|
}
|
|
506
|
+
return parts;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
/**
|
|
510
|
+
* The provider credit for `map_type` as plain text (no HTML), for the
|
|
511
|
+
* OpenLayers source `attributions`. Our own `.vco-map-attribution` line
|
|
512
|
+
* stays the rendered default and uses the linked `_getAttribution`
|
|
513
|
+
* version; this makes `source.getAttributions()` (and a consumer's
|
|
514
|
+
* `ol/control/Attribution`) work instead of returning nothing.
|
|
515
|
+
*/
|
|
516
|
+
_sourceAttributions(map_type: string): string[] {
|
|
517
|
+
const parts: string[] = [this._providerCredit(map_type).text];
|
|
206
518
|
if (this.options.attribution) {
|
|
207
519
|
parts.push(this.options.attribution);
|
|
208
520
|
}
|
|
209
|
-
return parts
|
|
521
|
+
return parts;
|
|
210
522
|
}
|
|
211
523
|
|
|
212
524
|
/* Create Tile Layer
|
|
@@ -222,12 +534,16 @@ export default class OpenLayers extends Map {
|
|
|
222
534
|
// to bury the route lines under fresh tiles
|
|
223
535
|
this._tile_layer.setZIndex(0);
|
|
224
536
|
this._map.addLayer(this._tile_layer);
|
|
537
|
+
// the story's own base layer: per-slide basemaps hand back to it
|
|
538
|
+
if (!this._story_basemap_layer) {
|
|
539
|
+
this._story_basemap_layer = this._tile_layer;
|
|
540
|
+
}
|
|
225
541
|
}
|
|
226
542
|
|
|
227
543
|
/**
|
|
228
544
|
* Ask for tile consent, then attach the layers if allowed.
|
|
229
545
|
*/
|
|
230
|
-
async _requestTileConsent(consent: ConsentManager, tile_service:
|
|
546
|
+
async _requestTileConsent(consent: ConsentManager, tile_service: ConsentService) {
|
|
231
547
|
if (await consent.request(tile_service, "", this._el.map)) {
|
|
232
548
|
this._onTilesAllowed();
|
|
233
549
|
}
|
|
@@ -240,24 +556,36 @@ export default class OpenLayers extends Map {
|
|
|
240
556
|
* Overlays sit above the base tiles (z 1..n) and below the route lines
|
|
241
557
|
* (z 10/11); every entry accepts any `map_type` value plus declarative
|
|
242
558
|
* presentation, so hosts no longer capture and patch layer objects.
|
|
559
|
+
* An entry carrying a `georeference` places a IIIF image on the
|
|
560
|
+
* geographic map instead of stacking a tile source.
|
|
243
561
|
*/
|
|
244
562
|
_buildOverlays(): void {
|
|
245
563
|
for (const layer of this._overlay_layers) {
|
|
246
564
|
this._map.removeLayer(layer);
|
|
247
565
|
}
|
|
248
566
|
this._overlay_layers = [];
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
if (
|
|
252
|
-
this.options.consent_required &&
|
|
253
|
-
consent &&
|
|
254
|
-
!consent.isGranted(tile_service)
|
|
255
|
-
) {
|
|
567
|
+
this._overlay_entries = [];
|
|
568
|
+
if (!this._tilesAllowed()) {
|
|
256
569
|
return;
|
|
257
570
|
}
|
|
258
571
|
const overlays = this.options.overlays ?? [];
|
|
259
572
|
overlays.forEach((entry, i) => {
|
|
260
|
-
|
|
573
|
+
let layer: Layer | null = null;
|
|
574
|
+
if (entry.georeference) {
|
|
575
|
+
layer = this._createGeoreferencedOverlay(entry);
|
|
576
|
+
} else if (entry.map_type) {
|
|
577
|
+
layer = this._createTileLayer(entry.map_type);
|
|
578
|
+
} else {
|
|
579
|
+
console.warn(
|
|
580
|
+
"StoryMapJS: an overlays[] entry has neither map_type nor georeference and was skipped:",
|
|
581
|
+
entry,
|
|
582
|
+
);
|
|
583
|
+
}
|
|
584
|
+
if (!layer) {
|
|
585
|
+
return;
|
|
586
|
+
}
|
|
587
|
+
// reserved z-range 1..n for stacked overlays (base tiles 0,
|
|
588
|
+
// route lines 10/11; see _addTileLayer)
|
|
261
589
|
layer.setZIndex(1 + i);
|
|
262
590
|
if (entry.opacity !== undefined) {
|
|
263
591
|
layer.setOpacity(entry.opacity);
|
|
@@ -280,11 +608,78 @@ export default class OpenLayers extends Map {
|
|
|
280
608
|
}
|
|
281
609
|
this._map.addLayer(layer);
|
|
282
610
|
this._overlay_layers.push(layer);
|
|
611
|
+
this._overlay_entries.push(entry);
|
|
283
612
|
this._paintOverlayBlend(entry);
|
|
284
613
|
});
|
|
285
614
|
this._syncOverlayAttributions();
|
|
286
615
|
}
|
|
287
616
|
|
|
617
|
+
/**
|
|
618
|
+
* Build a stacked overlay from a IIIF Georeference Extension payload
|
|
619
|
+
* (`overlays[].georeference`): the ground control points are fitted
|
|
620
|
+
* affinely and the image is placed as a geographic extent, which
|
|
621
|
+
* OpenLayers reprojects onto the view exactly like the IIIF basemap.
|
|
622
|
+
*
|
|
623
|
+
* The layer is returned empty and filled in asynchronously, mirroring
|
|
624
|
+
* the `iiif` basemap path — only the fit (pure, synchronous) decides
|
|
625
|
+
* whether a layer exists at all, so overlay indices stay stable.
|
|
626
|
+
*/
|
|
627
|
+
_createGeoreferencedOverlay(entry: StorymapOverlayLayer): TileLayer | null {
|
|
628
|
+
const georeference = entry.georeference;
|
|
629
|
+
if (!georeference) {
|
|
630
|
+
return null;
|
|
631
|
+
}
|
|
632
|
+
// image-space views (iiif/zoomify) have no geographic space to
|
|
633
|
+
// place the sheet in
|
|
634
|
+
if (this._map.getView().getProjection().getCode() !== "EPSG:3857") {
|
|
635
|
+
console.warn(
|
|
636
|
+
"StoryMapJS: a georeferenced overlay was skipped because this map has no geographic view (image maps cannot place sheets).",
|
|
637
|
+
);
|
|
638
|
+
return null;
|
|
639
|
+
}
|
|
640
|
+
const fit = fitGeoreference(georeference.body, georeference.width, georeference.height);
|
|
641
|
+
if (fit.kind === "skipped") {
|
|
642
|
+
console.warn(
|
|
643
|
+
`StoryMapJS: georeferenced overlay ${georeference.url} was skipped: ${fit.reason}.`,
|
|
644
|
+
);
|
|
645
|
+
return null;
|
|
646
|
+
}
|
|
647
|
+
const layer = new TileLayer();
|
|
648
|
+
const info_url = resolveInfoJsonUrl(georeference.url);
|
|
649
|
+
fetch(info_url)
|
|
650
|
+
.then((r) => r.json())
|
|
651
|
+
.then((info: unknown) => {
|
|
652
|
+
const parsed = new IIIFInfo(
|
|
653
|
+
info as ImageInformationResponse,
|
|
654
|
+
).getTileSourceOptions();
|
|
655
|
+
const size = info as { width?: number; height?: number };
|
|
656
|
+
if (typeof size.width !== "number" || typeof size.height !== "number") {
|
|
657
|
+
console.error("IIIF info.json is missing width/height:", info_url);
|
|
658
|
+
layer.setVisible(false);
|
|
659
|
+
return;
|
|
660
|
+
}
|
|
661
|
+
const geo_source = new IIIF({
|
|
662
|
+
...(parsed ?? {}),
|
|
663
|
+
projection: "EPSG:4326",
|
|
664
|
+
// the fitted corner box: the affine placement of the
|
|
665
|
+
// image in geographic coordinates
|
|
666
|
+
extent: fit.bbox,
|
|
667
|
+
size: [size.width, size.height],
|
|
668
|
+
crossOrigin: "anonymous",
|
|
669
|
+
attributions: entry.attribution ? [entry.attribution] : [],
|
|
670
|
+
});
|
|
671
|
+
layer.setSource(geo_source);
|
|
672
|
+
this._fireImageready(geo_source, "iiif", layer);
|
|
673
|
+
})
|
|
674
|
+
.catch((err) => {
|
|
675
|
+
// the layer stays in place (indices must not shift) but
|
|
676
|
+
// renders nothing; the console carries the reason
|
|
677
|
+
console.error("IIIF info.json could not be loaded:", info_url, err?.stack || err);
|
|
678
|
+
layer.setVisible(false);
|
|
679
|
+
});
|
|
680
|
+
return layer;
|
|
681
|
+
}
|
|
682
|
+
|
|
288
683
|
/**
|
|
289
684
|
* Convert a lon/lat clip box to view units. Only meaningful on
|
|
290
685
|
* mercator maps; image-space maps (iiif/zoomify) and malformed
|
|
@@ -310,10 +705,7 @@ export default class OpenLayers extends Map {
|
|
|
310
705
|
* frames (bounded: rendering settles within a frame or two of any
|
|
311
706
|
* add/visibility change).
|
|
312
707
|
*/
|
|
313
|
-
_paintOverlayBlend(
|
|
314
|
-
entry: { className?: string; blendMode?: string },
|
|
315
|
-
retries = 60,
|
|
316
|
-
): void {
|
|
708
|
+
_paintOverlayBlend(entry: { className?: string; blendMode?: string }, retries = 60): void {
|
|
317
709
|
if (!entry.blendMode || !entry.className) {
|
|
318
710
|
return;
|
|
319
711
|
}
|
|
@@ -337,10 +729,9 @@ export default class OpenLayers extends Map {
|
|
|
337
729
|
|
|
338
730
|
/** Refresh the attribution line with the visible overlays' credits. */
|
|
339
731
|
_syncOverlayAttributions(): void {
|
|
340
|
-
const overlays = this.options.overlays ?? [];
|
|
341
732
|
const parts: string[] = [];
|
|
342
733
|
this._overlay_layers.forEach((layer, i) => {
|
|
343
|
-
const credit =
|
|
734
|
+
const credit = this._overlay_entries[i]?.attribution;
|
|
344
735
|
if (credit && layer.getVisible()) {
|
|
345
736
|
parts.push(credit);
|
|
346
737
|
}
|
|
@@ -348,19 +739,38 @@ export default class OpenLayers extends Map {
|
|
|
348
739
|
this.setExtraAttributions(parts);
|
|
349
740
|
}
|
|
350
741
|
|
|
351
|
-
/**
|
|
742
|
+
/**
|
|
743
|
+
* Number of stacked overlay layers (see the `overlays` option). Alias:
|
|
744
|
+
* `getOverlayLayers().length`.
|
|
745
|
+
*/
|
|
352
746
|
getOverlayCount(): number {
|
|
353
747
|
return this._overlay_layers.length;
|
|
354
748
|
}
|
|
355
749
|
|
|
356
|
-
/**
|
|
750
|
+
/**
|
|
751
|
+
* The built overlay entries, parallel to `getOverlayLayers()`: malformed
|
|
752
|
+
* entries are skipped, so this is what a control must iterate to stay on
|
|
753
|
+
* the same index space as `setOverlayVisible()`.
|
|
754
|
+
*/
|
|
755
|
+
getOverlayEntries(): StorymapOverlayLayer[] {
|
|
756
|
+
return [...this._overlay_entries];
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
/**
|
|
760
|
+
* Show or hide a stacked overlay by index (re-syncs attribution). The
|
|
761
|
+
* index counts built layers, i.e. it skips malformed entries.
|
|
762
|
+
*
|
|
763
|
+
* Prefer `getOverlayLayer(index).setVisible(v)` on the layer itself; this
|
|
764
|
+
* wrapper exists because it also re-syncs the attribution line and the
|
|
765
|
+
* overlay blend mode, which a bare `setVisible()` does not.
|
|
766
|
+
*/
|
|
357
767
|
setOverlayVisible(index: number, visible: boolean): void {
|
|
358
768
|
const layer = this._overlay_layers[index];
|
|
359
769
|
if (!layer) {
|
|
360
770
|
return;
|
|
361
771
|
}
|
|
362
772
|
layer.setVisible(visible);
|
|
363
|
-
const entry =
|
|
773
|
+
const entry = this._overlay_entries[index];
|
|
364
774
|
if (entry && visible) {
|
|
365
775
|
this._paintOverlayBlend(entry);
|
|
366
776
|
}
|
|
@@ -368,6 +778,9 @@ export default class OpenLayers extends Map {
|
|
|
368
778
|
}
|
|
369
779
|
|
|
370
780
|
/** Set a stacked overlay's opacity by index. */
|
|
781
|
+
/**
|
|
782
|
+
* Alias: `getOverlayLayer(index)?.setOpacity(opacity)`.
|
|
783
|
+
*/
|
|
371
784
|
setOverlayOpacity(index: number, opacity: number): void {
|
|
372
785
|
this._overlay_layers[index]?.setOpacity(opacity);
|
|
373
786
|
}
|
|
@@ -379,6 +792,7 @@ export default class OpenLayers extends Map {
|
|
|
379
792
|
_onTilesAllowed(): void {
|
|
380
793
|
this._addTileLayer();
|
|
381
794
|
this._buildOverlays();
|
|
795
|
+
this.fire("tilesallowed");
|
|
382
796
|
if (!this._tile_layer_mini) {
|
|
383
797
|
this._tile_layer_mini = this._createTileLayer(this.options.map_type);
|
|
384
798
|
}
|
|
@@ -392,8 +806,14 @@ export default class OpenLayers extends Map {
|
|
|
392
806
|
const marker = this._markers[this.current_marker];
|
|
393
807
|
if (marker.data.type === "overview") {
|
|
394
808
|
this._markerOverview();
|
|
395
|
-
} else
|
|
396
|
-
|
|
809
|
+
} else {
|
|
810
|
+
const latlon = marker.latLon();
|
|
811
|
+
if (latlon) {
|
|
812
|
+
this._fitView(this._map, [[latlon.lon, latlon.lat]], 0);
|
|
813
|
+
}
|
|
814
|
+
// consent arrived after markers: the current slide may name
|
|
815
|
+
// a per-slide basemap
|
|
816
|
+
this._switchSlideBasemap(marker.data.location ?? null);
|
|
397
817
|
}
|
|
398
818
|
}
|
|
399
819
|
}
|
|
@@ -477,7 +897,373 @@ export default class OpenLayers extends Map {
|
|
|
477
897
|
return { zoom, center };
|
|
478
898
|
}
|
|
479
899
|
|
|
480
|
-
|
|
900
|
+
/**
|
|
901
|
+
* Paint the story's static image when its IIIF `info.json` cannot be
|
|
902
|
+
* used (unreachable, or missing dimensions): a single-resolution
|
|
903
|
+
* `ImageStatic` layer replaces the empty tile layer, so the tour still
|
|
904
|
+
* shows its picture instead of a blank basemap. Needs
|
|
905
|
+
* `iiif.fallbackUrl` (the full-size image) plus `iiif.width`/`height`
|
|
906
|
+
* (canvas size); without dimensions the natural size is probed, but
|
|
907
|
+
* only for slideshow tours — internal documents keep the existing
|
|
908
|
+
* error path. Returns false when nothing was (or will be) painted.
|
|
909
|
+
*/
|
|
910
|
+
_useStaticFallback(empty_layer: TileLayer): boolean {
|
|
911
|
+
const width = this.options.iiif.width;
|
|
912
|
+
const height = this.options.iiif.height;
|
|
913
|
+
const url = this.options.iiif.fallbackUrl;
|
|
914
|
+
if (typeof url !== "string" || url === "") {
|
|
915
|
+
return false;
|
|
916
|
+
}
|
|
917
|
+
if (typeof width === "number" && width > 0 && typeof height === "number" && height > 0) {
|
|
918
|
+
this._buildStaticLayer(empty_layer, url, width, height);
|
|
919
|
+
return true;
|
|
920
|
+
}
|
|
921
|
+
if (this.options.slideshow_source !== true) {
|
|
922
|
+
return false;
|
|
923
|
+
}
|
|
924
|
+
void this._probeStaticFallback(empty_layer, url);
|
|
925
|
+
return true;
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
/**
|
|
929
|
+
* Resolve a fallback image's natural size, then paint it. Stale probes
|
|
930
|
+
* (a newer probe started, e.g. after a basemap switch) and unusable
|
|
931
|
+
* dimensions fall back to the logged IIIF error.
|
|
932
|
+
*/
|
|
933
|
+
async _probeStaticFallback(empty_layer: TileLayer, url: string): Promise<void> {
|
|
934
|
+
const token = ++this._static_probe_token;
|
|
935
|
+
try {
|
|
936
|
+
const dims = await this._probeStaticImage(url);
|
|
937
|
+
if (this._disposed || token !== this._static_probe_token) return;
|
|
938
|
+
if (!dims || !(dims.width > 0) || !(dims.height > 0)) {
|
|
939
|
+
console.error("Static fallback image has no usable dimensions:", url);
|
|
940
|
+
return;
|
|
941
|
+
}
|
|
942
|
+
this._buildStaticLayer(empty_layer, url, dims.width, dims.height);
|
|
943
|
+
} catch (err) {
|
|
944
|
+
if (this._disposed || token !== this._static_probe_token) return;
|
|
945
|
+
console.error(
|
|
946
|
+
"Static fallback image could not be probed:",
|
|
947
|
+
url,
|
|
948
|
+
(err as Error)?.stack || err,
|
|
949
|
+
);
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
/**
|
|
954
|
+
* The natural size of an image URL. Separated for testability: unit
|
|
955
|
+
* tests stub the `Image` constructor rather than the network.
|
|
956
|
+
*/
|
|
957
|
+
_probeStaticImage(url: string): Promise<{ width: number; height: number }> {
|
|
958
|
+
return new Promise((resolve, reject) => {
|
|
959
|
+
const img = new Image();
|
|
960
|
+
const timer = setTimeout(() => {
|
|
961
|
+
reject(new Error(`timed out after ${STATIC_PROBE_TIMEOUT_MS}ms`));
|
|
962
|
+
}, STATIC_PROBE_TIMEOUT_MS);
|
|
963
|
+
img.onload = () => {
|
|
964
|
+
clearTimeout(timer);
|
|
965
|
+
resolve({ width: img.naturalWidth, height: img.naturalHeight });
|
|
966
|
+
};
|
|
967
|
+
img.onerror = () => {
|
|
968
|
+
clearTimeout(timer);
|
|
969
|
+
reject(new Error("load error"));
|
|
970
|
+
};
|
|
971
|
+
img.crossOrigin = "anonymous";
|
|
972
|
+
img.src = url;
|
|
973
|
+
});
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
/**
|
|
977
|
+
* One static image source: shared by the story fallback (known extent),
|
|
978
|
+
* per-slide static basemaps (probed extent) and per-slide overlays
|
|
979
|
+
* (view extent). Layer assembly stays per call site — base (z 0),
|
|
980
|
+
* pending-fill and overlay (z 5, opacity) have different lifecycles.
|
|
981
|
+
*/
|
|
982
|
+
_newStaticSource(
|
|
983
|
+
url: string,
|
|
984
|
+
extent: Extent,
|
|
985
|
+
projection: string | Projection,
|
|
986
|
+
attributions?: string | string[],
|
|
987
|
+
): Static {
|
|
988
|
+
return new Static({
|
|
989
|
+
url,
|
|
990
|
+
imageExtent: extent,
|
|
991
|
+
projection,
|
|
992
|
+
crossOrigin: "anonymous",
|
|
993
|
+
...(attributions !== undefined ? { attributions } : {}),
|
|
994
|
+
});
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
/**
|
|
998
|
+
* One static image layer: the constructor shared by the story fallback,
|
|
999
|
+
* per-slide static basemaps and per-slide overlays. Extent and
|
|
1000
|
+
* projection are the caller's policy (canvas size, probed size, or view
|
|
1001
|
+
* extent); presentation rides the options.
|
|
1002
|
+
*/
|
|
1003
|
+
_newStaticLayer(
|
|
1004
|
+
url: string,
|
|
1005
|
+
extent: Extent,
|
|
1006
|
+
projection: string | Projection,
|
|
1007
|
+
presentation: { opacity?: number; attributions?: string | string[]; zIndex?: number } = {},
|
|
1008
|
+
): ImageLayer<Static> {
|
|
1009
|
+
const layer = new ImageLayer({
|
|
1010
|
+
source: this._newStaticSource(url, extent, projection, presentation.attributions),
|
|
1011
|
+
...(presentation.opacity !== undefined ? { opacity: presentation.opacity } : {}),
|
|
1012
|
+
});
|
|
1013
|
+
layer.setZIndex(presentation.zIndex ?? 0);
|
|
1014
|
+
return layer;
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
/** Swap the empty tile layer for a static image of the given extent. */
|
|
1018
|
+
_buildStaticLayer(empty_layer: TileLayer, url: string, width: number, height: number): void {
|
|
1019
|
+
if (this._disposed) return;
|
|
1020
|
+
const layer = this._newStaticLayer(url, [0, 0, width, height], "EPSG:4326", {
|
|
1021
|
+
attributions: this.options.iiif.attribution || [],
|
|
1022
|
+
});
|
|
1023
|
+
if (this._tile_layer === (empty_layer as unknown as Layer)) {
|
|
1024
|
+
this._map.removeLayer(empty_layer);
|
|
1025
|
+
this._tile_layer = layer;
|
|
1026
|
+
this._map.addLayer(layer);
|
|
1027
|
+
} else {
|
|
1028
|
+
// superseded while resolving (basemap switch, dispose): announce
|
|
1029
|
+
// nothing and fit nothing for a layer that never shows
|
|
1030
|
+
return;
|
|
1031
|
+
}
|
|
1032
|
+
const source = this._sourceOf(layer);
|
|
1033
|
+
if (!source) return;
|
|
1034
|
+
this._fireImageready(source, "iiif", layer);
|
|
1035
|
+
if (typeof source.getState === "function" && source.getState() === "ready") {
|
|
1036
|
+
this._markerOverview();
|
|
1037
|
+
} else if (typeof (source as { once?: unknown }).once === "function") {
|
|
1038
|
+
(source as unknown as { once(e: string, fn: () => void): void }).once("change", () => {
|
|
1039
|
+
if (source.getState() === "ready") {
|
|
1040
|
+
this._markerOverview();
|
|
1041
|
+
}
|
|
1042
|
+
});
|
|
1043
|
+
} else {
|
|
1044
|
+
this._markerOverview();
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
_createTileLayer(map_type: string): Layer {
|
|
1049
|
+
// issue #473: custom OpenLayers tile layer/source factory first —
|
|
1050
|
+
// the base layer, overlays, minimap and runtime map_type switches
|
|
1051
|
+
// all funnel through here, so one check covers them
|
|
1052
|
+
const factory = this.options.tile_source_factory;
|
|
1053
|
+
if (typeof factory === "function") {
|
|
1054
|
+
const custom = factory(map_type, {
|
|
1055
|
+
options: this.options,
|
|
1056
|
+
createDefault: () => this._createDefaultTileLayer(map_type),
|
|
1057
|
+
});
|
|
1058
|
+
if (custom) {
|
|
1059
|
+
// any ol/layer/Layer passes through as-is — including
|
|
1060
|
+
// subclasses without a getSource (e.g. an Allmaps
|
|
1061
|
+
// WarpedMapLayer), which must not be wrapped; a bare Source is
|
|
1062
|
+
// wrapped in a TileLayer
|
|
1063
|
+
if (
|
|
1064
|
+
custom instanceof Layer ||
|
|
1065
|
+
typeof (custom as { getSource?: unknown }).getSource === "function"
|
|
1066
|
+
) {
|
|
1067
|
+
return custom as Layer;
|
|
1068
|
+
}
|
|
1069
|
+
return new TileLayer({ source: custom as unknown as XYZ });
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
return this._createDefaultTileLayer(map_type);
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
/**
|
|
1076
|
+
* A layer's source, or null when it has none. Custom `Layer` subclasses
|
|
1077
|
+
* passed through `_createTileLayer` (e.g. Allmaps' `WarpedMapLayer`)
|
|
1078
|
+
* deliberately have no `getSource`, so every read goes through here
|
|
1079
|
+
* instead of calling the method directly.
|
|
1080
|
+
*/
|
|
1081
|
+
_sourceOf(layer: Layer | null | undefined): Source | null {
|
|
1082
|
+
if (!layer || typeof (layer as { getSource?: unknown }).getSource !== "function") {
|
|
1083
|
+
return null;
|
|
1084
|
+
}
|
|
1085
|
+
return (layer as TileLayer).getSource();
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
/**
|
|
1089
|
+
* Move the map into another element, keeping OpenLayers in step.
|
|
1090
|
+
*
|
|
1091
|
+
* `DomMixed.addTo()` (the original semantics, unchanged: append the map
|
|
1092
|
+
* container, fire `added`) only moves DOM — the `ol/Map` still has the
|
|
1093
|
+
* viewport it measured at construction, so it would keep painting at the
|
|
1094
|
+
* old size. Re-attach the target, which `removeFrom()` detached, and
|
|
1095
|
+
* re-measure. The argument must be the new parent.
|
|
1096
|
+
*/
|
|
1097
|
+
addTo(container: HTMLElement): this {
|
|
1098
|
+
super.addTo(container);
|
|
1099
|
+
this._map.setTarget(this._el.map);
|
|
1100
|
+
this._map.updateSize();
|
|
1101
|
+
return this;
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
/**
|
|
1105
|
+
* Move the map out of `container`. Untargets the OpenLayers map so it
|
|
1106
|
+
* does not render into a detached node; a later {@link addTo} re-attaches
|
|
1107
|
+
* it. The argument must be the current parent.
|
|
1108
|
+
*/
|
|
1109
|
+
removeFrom(container: HTMLElement): this {
|
|
1110
|
+
super.removeFrom(container);
|
|
1111
|
+
this._map.setTarget(undefined);
|
|
1112
|
+
return this;
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
/**
|
|
1116
|
+
* True when the map is a plain image in an `EPSG:4326` "image space"
|
|
1117
|
+
* rather than a geographic map: a IIIF image presented as a picture of
|
|
1118
|
+
* the world (`map_as_image`), not georeferenced.
|
|
1119
|
+
*
|
|
1120
|
+
* In this mode the view projection is `EPSG:4326` with degrees as
|
|
1121
|
+
* resolution and the view is fitted to the image extent, so
|
|
1122
|
+
* `getCenter()`/`getZoom()` are in degrees/pixels-per-degree — not
|
|
1123
|
+
* mercator metres. Marker locations are pixel offsets into the image,
|
|
1124
|
+
* not lat/lon. Georeferenced IIIF (`map_bbox` with `map_as_image`
|
|
1125
|
+
* omitted) is a normal mercator map; use `isImageSpace()` rather than
|
|
1126
|
+
* testing `map_type` to tell the two apart.
|
|
1127
|
+
*/
|
|
1128
|
+
isImageSpace(): boolean {
|
|
1129
|
+
return this.options.map_type === "iiif" && this.options.map_as_image === true;
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
/**
|
|
1133
|
+
* Stop the line animation, drop the minimap control and dispose the
|
|
1134
|
+
* OpenLayers map. Called by `StoryMap.dispose()`; the engine must not be
|
|
1135
|
+
* used afterwards.
|
|
1136
|
+
*/
|
|
1137
|
+
dispose(): void {
|
|
1138
|
+
this._disposed = true;
|
|
1139
|
+
// invalidate in-flight static probes and info.json fetches: their
|
|
1140
|
+
// continuations check the flag or their token and resolve quietly
|
|
1141
|
+
this._static_probe_token++;
|
|
1142
|
+
if (this._line_animation !== null) {
|
|
1143
|
+
cancelAnimationFrame(this._line_animation);
|
|
1144
|
+
this._line_animation = null;
|
|
1145
|
+
}
|
|
1146
|
+
if (this._loadend_key) {
|
|
1147
|
+
unByKey(this._loadend_key);
|
|
1148
|
+
this._loadend_key = null;
|
|
1149
|
+
}
|
|
1150
|
+
if (this._onWheelBound) {
|
|
1151
|
+
this._el.map?.removeEventListener("wheel", this._onWheelBound);
|
|
1152
|
+
this._onWheelBound = null;
|
|
1153
|
+
}
|
|
1154
|
+
// markers own their click listener and an overlay; disposing the
|
|
1155
|
+
// collection releases both
|
|
1156
|
+
for (const marker of this._markers) {
|
|
1157
|
+
marker.dispose?.();
|
|
1158
|
+
}
|
|
1159
|
+
this._markers = [];
|
|
1160
|
+
this._mask_el?.remove();
|
|
1161
|
+
this._mask_el = null;
|
|
1162
|
+
this._basemap_cache.clear();
|
|
1163
|
+
this._slide_probe_tokens.clear();
|
|
1164
|
+
this._slide_basemap = null;
|
|
1165
|
+
this._story_basemap_layer = null;
|
|
1166
|
+
this._imgoverlay_layer = null;
|
|
1167
|
+
// detach the viewport first: dispose() alone leaves the canvas in
|
|
1168
|
+
// the caller's DOM
|
|
1169
|
+
this._map.setTarget(undefined);
|
|
1170
|
+
this._map.dispose();
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
/**
|
|
1174
|
+
* Fire `imageready` once `source` is usable (and immediately when it
|
|
1175
|
+
* already is). Image sources — IIIF, zoomify, the deferred tile layer —
|
|
1176
|
+
* attach asynchronously, so this is the only outward signal that the
|
|
1177
|
+
* imagery is really on the map; the `loaded` event can fire before the
|
|
1178
|
+
* source is attached at all.
|
|
1179
|
+
*/
|
|
1180
|
+
_fireImageready(source: { getState?(): string }, kind: ImagereadyKind, layer?: Layer): void {
|
|
1181
|
+
// one event per source: the base layer and the minimap can be asked
|
|
1182
|
+
// about the same source, and a host toggling layers should not have
|
|
1183
|
+
// to filter duplicates
|
|
1184
|
+
if (this._imageready_fired.has(source)) return;
|
|
1185
|
+
this._imageready_fired.add(source);
|
|
1186
|
+
const payload = { source, kind, layer: layer ?? null };
|
|
1187
|
+
if (typeof source.getState !== "function" || source.getState() === "ready") {
|
|
1188
|
+
this.fire("imageready", payload);
|
|
1189
|
+
return;
|
|
1190
|
+
}
|
|
1191
|
+
const onChange = () => {
|
|
1192
|
+
if (source.getState?.() === "ready") {
|
|
1193
|
+
(source as { un?: (t: string, f: () => void) => void }).un?.("change", onChange);
|
|
1194
|
+
this.fire("imageready", payload);
|
|
1195
|
+
}
|
|
1196
|
+
};
|
|
1197
|
+
(source as { once?: (t: string, f: () => void) => void }).once?.("change", onChange);
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
/** Computed zooms take precedence over the authored slide zoom. */
|
|
1201
|
+
_markerZoom(index: number): number | undefined {
|
|
1202
|
+
return this._marker_zooms[index] ?? super._markerZoom(index);
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
/**
|
|
1206
|
+
* The TileJSON metadata, but only when it is describing `url`.
|
|
1207
|
+
*
|
|
1208
|
+
* `tilejson` describes the map's tile source, so its `tiles` template is
|
|
1209
|
+
* the one the map_type resolved to. When it names a different template it
|
|
1210
|
+
* is not about this source, and neither the zoom ladder nor the initial
|
|
1211
|
+
* view should be taken from it.
|
|
1212
|
+
*/
|
|
1213
|
+
_tilejsonFor(url: string): StorymapTilejson | undefined {
|
|
1214
|
+
const tilejson = this.options.tilejson;
|
|
1215
|
+
if (tilejson === undefined) return undefined;
|
|
1216
|
+
const tiles = Array.isArray(tilejson.tiles) ? tilejson.tiles[0] : tilejson.tiles;
|
|
1217
|
+
return tiles === url ? tilejson : undefined;
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
/**
|
|
1221
|
+
* An XYZ source for a URL template, honouring the TileJSON metadata of the
|
|
1222
|
+
* map's own tile source if it is describing this template (§2.9).
|
|
1223
|
+
*
|
|
1224
|
+
* `minzoom`/`maxzoom` become the tile grid's zoom range, so OpenLayers asks
|
|
1225
|
+
* for levels the service actually has and stops past the ones it does not.
|
|
1226
|
+
* `scheme: "tms"` is the TMS row order, which is the same URL with the tile
|
|
1227
|
+
* row counted from the bottom.
|
|
1228
|
+
*/
|
|
1229
|
+
_xyzSource(url: string): XYZ {
|
|
1230
|
+
const tilejson = this._tilejsonFor(url);
|
|
1231
|
+
const options: Record<string, unknown> = {
|
|
1232
|
+
url,
|
|
1233
|
+
attributions: this._sourceAttributions(url),
|
|
1234
|
+
crossOrigin: "anonymous",
|
|
1235
|
+
};
|
|
1236
|
+
if (tilejson !== undefined) {
|
|
1237
|
+
if (tilejson.minzoom !== undefined || tilejson.maxzoom !== undefined) {
|
|
1238
|
+
options.minZoom = tilejson.minzoom ?? 0;
|
|
1239
|
+
options.maxZoom = tilejson.maxzoom ?? MAX_ZOOM;
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
const source = new XYZ(options as never);
|
|
1243
|
+
if (tilejson?.scheme === "tms") {
|
|
1244
|
+
// TMS numbers tile rows from the bottom of the pyramid; XYZ counts
|
|
1245
|
+
// them from the top, so the row is flipped against the source's
|
|
1246
|
+
// deepest zoom.
|
|
1247
|
+
//
|
|
1248
|
+
// This is assigned rather than passed as the `tileUrlFunction`
|
|
1249
|
+
// constructor option, which this version of ol/source/XYZ does not
|
|
1250
|
+
// read.
|
|
1251
|
+
const deepest = tilejson.maxzoom ?? MAX_ZOOM;
|
|
1252
|
+
source.tileUrlFunction = (tileCoord: number[] | undefined): string | undefined => {
|
|
1253
|
+
if (tileCoord === undefined) return undefined;
|
|
1254
|
+
// OpenLayers hands over [z, x, y]
|
|
1255
|
+
const [z, x, y] = tileCoord;
|
|
1256
|
+
return url
|
|
1257
|
+
.replace("{z}", String(z))
|
|
1258
|
+
.replace("{x}", String(x))
|
|
1259
|
+
.replace("{y}", String(2 ** deepest - 1 - y))
|
|
1260
|
+
.replace("{ratio}", "");
|
|
1261
|
+
};
|
|
1262
|
+
}
|
|
1263
|
+
return source;
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
_createDefaultTileLayer(map_type: string): TileLayer {
|
|
481
1267
|
const _map_type_arr = map_type.split(":");
|
|
482
1268
|
|
|
483
1269
|
switch (_map_type_arr[0]) {
|
|
@@ -494,7 +1280,7 @@ export default class OpenLayers extends Map {
|
|
|
494
1280
|
return new TileLayer({
|
|
495
1281
|
source: new XYZ({
|
|
496
1282
|
url: mapbox_url,
|
|
497
|
-
attributions:
|
|
1283
|
+
attributions: this._sourceAttributions(map_type),
|
|
498
1284
|
crossOrigin: "anonymous",
|
|
499
1285
|
}),
|
|
500
1286
|
});
|
|
@@ -502,11 +1288,18 @@ export default class OpenLayers extends Map {
|
|
|
502
1288
|
console.error(
|
|
503
1289
|
"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
1290
|
);
|
|
505
|
-
return new TileLayer({
|
|
1291
|
+
return new TileLayer({
|
|
1292
|
+
source: new OSM({ attributions: this._sourceAttributions(map_type) }),
|
|
1293
|
+
});
|
|
506
1294
|
}
|
|
507
1295
|
|
|
508
1296
|
case "stadia": {
|
|
509
|
-
|
|
1297
|
+
// Stadia hosts a style *set*, not a single style. A bare
|
|
1298
|
+
// `map_type: "stadia"` used to fall back to the literal
|
|
1299
|
+
// "osm:standard", which is not one of them — every tile 404'd.
|
|
1300
|
+
// `alidade_smooth` is the correct default here (matches the
|
|
1301
|
+
// remapped `stamen` case below).
|
|
1302
|
+
let style_url = "alidade_smooth";
|
|
510
1303
|
if (_map_type_arr.length > 1) {
|
|
511
1304
|
style_url = _map_type_arr.slice(1).join(":");
|
|
512
1305
|
if (this.options.map_access_token) {
|
|
@@ -515,8 +1308,8 @@ export default class OpenLayers extends Map {
|
|
|
515
1308
|
}
|
|
516
1309
|
return new TileLayer({
|
|
517
1310
|
source: new XYZ({
|
|
518
|
-
url: `https://tiles.stadiamaps.com/tiles/${style_url}/{z}/{x}/{y}.png`,
|
|
519
|
-
attributions:
|
|
1311
|
+
url: `https://tiles.stadiamaps.com/tiles/${style_url}/{z}/{x}/{y}{r}.png`,
|
|
1312
|
+
attributions: this._sourceAttributions(map_type),
|
|
520
1313
|
}),
|
|
521
1314
|
});
|
|
522
1315
|
}
|
|
@@ -526,12 +1319,23 @@ export default class OpenLayers extends Map {
|
|
|
526
1319
|
return new TileLayer({
|
|
527
1320
|
source: new XYZ({
|
|
528
1321
|
url: "https://tiles.stadiamaps.com/tiles/stamen_toner_lite/{z}/{x}/{y}.png",
|
|
529
|
-
attributions:
|
|
1322
|
+
attributions: this._sourceAttributions(map_type),
|
|
530
1323
|
}),
|
|
531
1324
|
});
|
|
532
1325
|
|
|
533
1326
|
case "iiif": {
|
|
534
1327
|
const iiif_layer: TileLayer = new TileLayer();
|
|
1328
|
+
// a static-image tour states no info.json at all: skip the
|
|
1329
|
+
// fetch and go straight to the fallback (which probes when
|
|
1330
|
+
// no dimensions are stated either)
|
|
1331
|
+
if (typeof this.options.iiif.url !== "string" || this.options.iiif.url === "") {
|
|
1332
|
+
if (!this._useStaticFallback(iiif_layer)) {
|
|
1333
|
+
console.error(
|
|
1334
|
+
"IIIF info.json URL is missing and no static fallback is stated.",
|
|
1335
|
+
);
|
|
1336
|
+
}
|
|
1337
|
+
return iiif_layer;
|
|
1338
|
+
}
|
|
535
1339
|
fetch(this.options.iiif.url)
|
|
536
1340
|
.then((r) => r.json())
|
|
537
1341
|
.then((info: unknown) => {
|
|
@@ -546,10 +1350,15 @@ export default class OpenLayers extends Map {
|
|
|
546
1350
|
typeof fallback.width !== "number" ||
|
|
547
1351
|
typeof fallback.height !== "number"
|
|
548
1352
|
) {
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
)
|
|
1353
|
+
// without dimensions there is no grid to build;
|
|
1354
|
+
// fall back to the static image when the story
|
|
1355
|
+
// states dimensions of its own
|
|
1356
|
+
if (!this._useStaticFallback(iiif_layer)) {
|
|
1357
|
+
console.error(
|
|
1358
|
+
"IIIF info.json is missing width/height:",
|
|
1359
|
+
this.options.iiif.url,
|
|
1360
|
+
);
|
|
1361
|
+
}
|
|
553
1362
|
return;
|
|
554
1363
|
}
|
|
555
1364
|
const source = new IIIF({
|
|
@@ -560,6 +1369,7 @@ export default class OpenLayers extends Map {
|
|
|
560
1369
|
attributions: this.options.iiif.attribution || [],
|
|
561
1370
|
});
|
|
562
1371
|
iiif_layer.setSource(source);
|
|
1372
|
+
this._fireImageready(source, "iiif", iiif_layer);
|
|
563
1373
|
if (source.getState() === "ready") {
|
|
564
1374
|
this._markerOverview();
|
|
565
1375
|
} else {
|
|
@@ -571,11 +1381,18 @@ export default class OpenLayers extends Map {
|
|
|
571
1381
|
}
|
|
572
1382
|
})
|
|
573
1383
|
.catch((err) =>
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
1384
|
+
// the tile pyramid is unreachable: paint the static
|
|
1385
|
+
// image instead when the story states one, so the
|
|
1386
|
+
// tour still shows its picture
|
|
1387
|
+
{
|
|
1388
|
+
if (!this._useStaticFallback(iiif_layer)) {
|
|
1389
|
+
console.error(
|
|
1390
|
+
"IIIF info.json could not be loaded:",
|
|
1391
|
+
this.options.iiif.url,
|
|
1392
|
+
err?.stack || err,
|
|
1393
|
+
);
|
|
1394
|
+
}
|
|
1395
|
+
},
|
|
579
1396
|
);
|
|
580
1397
|
return iiif_layer;
|
|
581
1398
|
}
|
|
@@ -587,15 +1404,13 @@ export default class OpenLayers extends Map {
|
|
|
587
1404
|
if (!map_type.includes("{z}")) {
|
|
588
1405
|
return this._createVectorStyleLayer(map_type);
|
|
589
1406
|
}
|
|
590
|
-
return new TileLayer({
|
|
591
|
-
source: new XYZ({ url: map_type, attributions: [], crossOrigin: "anonymous" }),
|
|
592
|
-
});
|
|
1407
|
+
return new TileLayer({ source: this._xyzSource(map_type) });
|
|
593
1408
|
|
|
594
1409
|
case "ch-watercolor":
|
|
595
1410
|
return new TileLayer({
|
|
596
1411
|
source: new XYZ({
|
|
597
1412
|
url: "https://watercolormaps.collection.cooperhewitt.org/tile/watercolor/{z}/{x}/{y}.jpg",
|
|
598
|
-
attributions:
|
|
1413
|
+
attributions: this._sourceAttributions(map_type),
|
|
599
1414
|
maxZoom: 16,
|
|
600
1415
|
}),
|
|
601
1416
|
});
|
|
@@ -611,50 +1426,56 @@ export default class OpenLayers extends Map {
|
|
|
611
1426
|
console.error(
|
|
612
1427
|
"StoryMapJS: map_type 'zoomify' needs a zoomify image pyramid (path, width, height) in the storymap data.",
|
|
613
1428
|
);
|
|
614
|
-
return new TileLayer({
|
|
1429
|
+
return new TileLayer({
|
|
1430
|
+
source: new OSM({ attributions: this._sourceAttributions(map_type) }),
|
|
1431
|
+
});
|
|
615
1432
|
}
|
|
616
1433
|
const path = (this.options.zoomify as { path?: string }).path ?? "";
|
|
617
1434
|
const { sizes, maxZoom: pyramidMaxZoom } = pyramid;
|
|
618
1435
|
const gridX = (z: number) => Math.ceil(sizes[z][0] / 256);
|
|
619
1436
|
const gridY = (z: number) => Math.ceil(sizes[z][1] / 256);
|
|
620
1437
|
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
},
|
|
656
|
-
}),
|
|
1438
|
+
const zoomify_source = new XYZ({
|
|
1439
|
+
tileGrid: pyramid.tileGrid,
|
|
1440
|
+
crossOrigin: "anonymous",
|
|
1441
|
+
attributions: this._sourceAttributions(map_type),
|
|
1442
|
+
tileUrlFunction: (tile: number[]) => {
|
|
1443
|
+
const [tileZ, x, y] = tile;
|
|
1444
|
+
// the ladder is shifted one level down: mercator
|
|
1445
|
+
// zoom z serves the pyramid level max(0, z - 1)
|
|
1446
|
+
const z = Math.max(0, tileZ - 1);
|
|
1447
|
+
if (z > pyramidMaxZoom) return undefined;
|
|
1448
|
+
if (x < 0 || x >= gridX(z) || y < 0 || y >= gridY(z)) {
|
|
1449
|
+
return undefined;
|
|
1450
|
+
}
|
|
1451
|
+
// TileGroup index: the running tile number ÷ 256
|
|
1452
|
+
let num = 0;
|
|
1453
|
+
for (let zz = 0; zz < z; zz++) {
|
|
1454
|
+
num += gridX(zz) * gridY(zz);
|
|
1455
|
+
}
|
|
1456
|
+
num += y * gridX(z) + x;
|
|
1457
|
+
return `${path}TileGroup${Math.floor(num / 256)}/${z}-${x}-${y}.jpg`;
|
|
1458
|
+
},
|
|
1459
|
+
// Zoomify edge tiles are cropped to the image bounds
|
|
1460
|
+
// (e.g. a 256x19 bottom strip); OpenLayers draws the
|
|
1461
|
+
// loaded image over the whole 256x256 tile box, which
|
|
1462
|
+
// stretched those strips across the cell (the smeared
|
|
1463
|
+
// bottom in the Bosch overview, stretched right/bottom
|
|
1464
|
+
// edges in the Literary Trail). Pad them onto a full
|
|
1465
|
+
// tile canvas instead.
|
|
1466
|
+
tileLoadFunction: (tile, src) => {
|
|
1467
|
+
const imageTile = tile as ImageTile;
|
|
1468
|
+
const image = imageTile.getImage() as HTMLImageElement;
|
|
1469
|
+
image.onload = () => padCroppedZoomifyTile(imageTile, image);
|
|
1470
|
+
image.src = src;
|
|
1471
|
+
},
|
|
657
1472
|
});
|
|
1473
|
+
const zoomify_layer = new TileLayer({ source: zoomify_source });
|
|
1474
|
+
// the zoomify base never announced itself, so a host waiting
|
|
1475
|
+
// on imageready hung on exactly the legacy stories that need
|
|
1476
|
+
// it most; the per-source dedup keeps this to one event
|
|
1477
|
+
this._fireImageready(zoomify_source, "zoomify", zoomify_layer);
|
|
1478
|
+
return zoomify_layer;
|
|
658
1479
|
}
|
|
659
1480
|
|
|
660
1481
|
case "osm": {
|
|
@@ -667,7 +1488,9 @@ export default class OpenLayers extends Map {
|
|
|
667
1488
|
`https://tiles.openfreemap.org/styles/${style_name}`,
|
|
668
1489
|
);
|
|
669
1490
|
}
|
|
670
|
-
return new TileLayer({
|
|
1491
|
+
return new TileLayer({
|
|
1492
|
+
source: new OSM({ attributions: this._sourceAttributions(map_type) }),
|
|
1493
|
+
});
|
|
671
1494
|
}
|
|
672
1495
|
default: {
|
|
673
1496
|
// Relative/custom templates (./tiles/{z}/{x}/{y}.png,
|
|
@@ -678,18 +1501,14 @@ export default class OpenLayers extends Map {
|
|
|
678
1501
|
// {z} render as vector styles, anything else falls back to
|
|
679
1502
|
// OSM.
|
|
680
1503
|
if (map_type.includes("{z}")) {
|
|
681
|
-
return new TileLayer({
|
|
682
|
-
source: new XYZ({
|
|
683
|
-
url: map_type,
|
|
684
|
-
attributions: [],
|
|
685
|
-
crossOrigin: "anonymous",
|
|
686
|
-
}),
|
|
687
|
-
});
|
|
1504
|
+
return new TileLayer({ source: this._xyzSource(map_type) });
|
|
688
1505
|
}
|
|
689
1506
|
if (map_type.includes("/") || map_type.endsWith(".json")) {
|
|
690
1507
|
return this._createVectorStyleLayer(map_type);
|
|
691
1508
|
}
|
|
692
|
-
return new TileLayer({
|
|
1509
|
+
return new TileLayer({
|
|
1510
|
+
source: new OSM({ attributions: this._sourceAttributions(map_type) }),
|
|
1511
|
+
});
|
|
693
1512
|
}
|
|
694
1513
|
}
|
|
695
1514
|
}
|
|
@@ -718,15 +1537,10 @@ export default class OpenLayers extends Map {
|
|
|
718
1537
|
// consent mode: the minimap layer is only created once map tiles are
|
|
719
1538
|
// allowed — creating an IIIF/vector layer object fetches info.json or
|
|
720
1539
|
// the style JSON immediately, which must not happen while denied
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
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;
|
|
1540
|
+
this._tile_layer_mini = this._tilesAllowed()
|
|
1541
|
+
? this._createTileLayer(this.options.map_type)
|
|
1542
|
+
: null;
|
|
1543
|
+
const is_image_map = this.isImageSpace();
|
|
730
1544
|
// Legacy zoomify maps are mercator-based: the minimap fits the image's
|
|
731
1545
|
// mercator bounds with a free zoom so the whole image stays visible
|
|
732
1546
|
// at a downscaled (sharp) pyramid level
|
|
@@ -828,9 +1642,7 @@ export default class OpenLayers extends Map {
|
|
|
828
1642
|
// the minimap starts collapsed (no layout size yet) in its
|
|
829
1643
|
// 150x100 box — fall back to that until it expands
|
|
830
1644
|
const size =
|
|
831
|
-
raw_size && raw_size[0] >= 50 && raw_size[1] >= 50
|
|
832
|
-
? raw_size
|
|
833
|
-
: [150, 100];
|
|
1645
|
+
raw_size && raw_size[0] >= 50 && raw_size[1] >= 50 ? raw_size : [150, 100];
|
|
834
1646
|
overview_map.getView().fit(zoomify_pyramid.extent, {
|
|
835
1647
|
size: size,
|
|
836
1648
|
});
|
|
@@ -842,10 +1654,15 @@ export default class OpenLayers extends Map {
|
|
|
842
1654
|
} else if (!is_zoomify && this.bounds_array && this.bounds_array.length) {
|
|
843
1655
|
// the minimap shows the story's world: with a bbox set, markers
|
|
844
1656
|
// outside of the box are unreachable and must not skew the fit
|
|
845
|
-
this._fitView(
|
|
1657
|
+
this._fitView(
|
|
1658
|
+
this._mini_map.getOverviewMap(),
|
|
1659
|
+
this.bounds_array,
|
|
1660
|
+
0,
|
|
1661
|
+
this._bboxExtent() !== null,
|
|
1662
|
+
);
|
|
846
1663
|
}
|
|
847
1664
|
|
|
848
|
-
if (this.
|
|
1665
|
+
if (this.isImageSpace()) {
|
|
849
1666
|
// in image mode there are no geo markers to fit, so show the
|
|
850
1667
|
// whole image instead (issues #465, #355)
|
|
851
1668
|
this._fitMiniMapToImage();
|
|
@@ -856,7 +1673,7 @@ export default class OpenLayers extends Map {
|
|
|
856
1673
|
if (!this._tile_layer_mini) return;
|
|
857
1674
|
const fit_mini_image = () => {
|
|
858
1675
|
try {
|
|
859
|
-
const mini_source = this._tile_layer_mini
|
|
1676
|
+
const mini_source = this._sourceOf(this._tile_layer_mini) as {
|
|
860
1677
|
getTileGrid?: () => { getExtent(): number[] };
|
|
861
1678
|
} | null;
|
|
862
1679
|
const grid = mini_source?.getTileGrid?.();
|
|
@@ -874,8 +1691,17 @@ export default class OpenLayers extends Map {
|
|
|
874
1691
|
console.warn("IIIF minimap fit failed:", e);
|
|
875
1692
|
}
|
|
876
1693
|
};
|
|
877
|
-
const mini_source = this._tile_layer_mini
|
|
1694
|
+
const mini_source = this._sourceOf(this._tile_layer_mini);
|
|
878
1695
|
if (mini_source) {
|
|
1696
|
+
this._fireImageready(
|
|
1697
|
+
mini_source,
|
|
1698
|
+
this.options.map_type === "zoomify"
|
|
1699
|
+
? "zoomify"
|
|
1700
|
+
: this.options.map_type === "iiif"
|
|
1701
|
+
? "iiif"
|
|
1702
|
+
: "tiles",
|
|
1703
|
+
this._tile_layer_mini,
|
|
1704
|
+
);
|
|
879
1705
|
if (mini_source.getState() === "ready") {
|
|
880
1706
|
fit_mini_image();
|
|
881
1707
|
} else {
|
|
@@ -886,7 +1712,7 @@ export default class OpenLayers extends Map {
|
|
|
886
1712
|
} else {
|
|
887
1713
|
// the mini layer sets its source asynchronously
|
|
888
1714
|
this._tile_layer_mini.once("change:source", () => {
|
|
889
|
-
const src = this._tile_layer_mini
|
|
1715
|
+
const src = this._sourceOf(this._tile_layer_mini);
|
|
890
1716
|
if (!src) return;
|
|
891
1717
|
if (src.getState() === "ready") {
|
|
892
1718
|
fit_mini_image();
|
|
@@ -904,6 +1730,10 @@ export default class OpenLayers extends Map {
|
|
|
904
1730
|
_createMarker(d: StorymapSlide): void {
|
|
905
1731
|
const marker = new OpenLayersMapMarker(d, this.options);
|
|
906
1732
|
marker.on("markerclick", this._onMarkerClick, this);
|
|
1733
|
+
// Forward popup cards to the map surface (fresh payloads, so the
|
|
1734
|
+
// marker stays the event target of the original, not the re-fire).
|
|
1735
|
+
marker.on("popupopen", this._onMarkerPopupOpen, this);
|
|
1736
|
+
marker.on("popupclose", this._onMarkerPopupClose, this);
|
|
907
1737
|
this._addMarker(marker);
|
|
908
1738
|
this._markers.push(marker);
|
|
909
1739
|
marker.marker_number = this._markers.length - 1;
|
|
@@ -923,12 +1753,16 @@ export default class OpenLayers extends Map {
|
|
|
923
1753
|
if (this._map.getView().getProjection().getCode() === "EPSG:4326") {
|
|
924
1754
|
return;
|
|
925
1755
|
}
|
|
926
|
-
const real =
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
1756
|
+
const real: { marker: OpenLayersMapMarker; latlon: LatLngLiteral }[] = [];
|
|
1757
|
+
for (const marker of this._markers) {
|
|
1758
|
+
const latlon = marker.latLon();
|
|
1759
|
+
if (latlon) {
|
|
1760
|
+
real.push({ marker: marker, latlon: latlon });
|
|
1761
|
+
}
|
|
1762
|
+
}
|
|
1763
|
+
const unwrapped = this._unwrapLongitudes(real.map((entry) => entry.latlon.lon));
|
|
1764
|
+
real.forEach((entry, i) => {
|
|
1765
|
+
entry.marker._overlay?.setPosition(fromLonLat([unwrapped[i], entry.latlon.lat]));
|
|
932
1766
|
});
|
|
933
1767
|
}
|
|
934
1768
|
|
|
@@ -941,18 +1775,16 @@ export default class OpenLayers extends Map {
|
|
|
941
1775
|
/* Marker helpers
|
|
942
1776
|
================================================== */
|
|
943
1777
|
_getAllMarkersBounds(markers_array: OpenLayersMapMarker[]): number[][] {
|
|
944
|
-
const coords = [];
|
|
945
|
-
for (
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
markers_array[i].data.location.lat,
|
|
950
|
-
]);
|
|
1778
|
+
const coords: number[][] = [];
|
|
1779
|
+
for (const marker of markers_array) {
|
|
1780
|
+
const latlon = marker.latLon();
|
|
1781
|
+
if (latlon) {
|
|
1782
|
+
coords.push([latlon.lon, latlon.lat]);
|
|
951
1783
|
}
|
|
952
1784
|
}
|
|
953
1785
|
// unwrap dateline crossings so fits don't span the whole globe
|
|
954
1786
|
// (issue #381)
|
|
955
|
-
const lons = this._unwrapLongitudes(coords.map((c) => c[0]
|
|
1787
|
+
const lons = this._unwrapLongitudes(coords.map((c) => c[0]));
|
|
956
1788
|
return coords.map((c, i) => [lons[i], c[1]]);
|
|
957
1789
|
}
|
|
958
1790
|
|
|
@@ -986,8 +1818,7 @@ export default class OpenLayers extends Map {
|
|
|
986
1818
|
_bboxExtent(): number[] | null {
|
|
987
1819
|
const bbox = this.options.map_bbox as number[] | null | undefined;
|
|
988
1820
|
if (!bbox || bbox.length !== 4) return null;
|
|
989
|
-
|
|
990
|
-
if (is_image_space) return bbox;
|
|
1821
|
+
if (this.isImageSpace()) return bbox;
|
|
991
1822
|
// NB: fromLonLat transforms a single [lon, lat] pair — transform the
|
|
992
1823
|
// two corners separately
|
|
993
1824
|
const min = fromLonLat([bbox[0], bbox[1]]);
|
|
@@ -1004,7 +1835,17 @@ export default class OpenLayers extends Map {
|
|
|
1004
1835
|
_opaquePanelPadding(): [number, number, number, number] {
|
|
1005
1836
|
const padding: [number, number, number, number] = [15, 15, 15, 15];
|
|
1006
1837
|
if (typeof document === "undefined") return padding;
|
|
1007
|
-
|
|
1838
|
+
// Scope the search to THIS viewer's root. The slide panel is a sibling
|
|
1839
|
+
// of the map, not a descendant, so it is not reachable from
|
|
1840
|
+
// this._el.container (the .vco-map div); a document-wide query would
|
|
1841
|
+
// find whichever panel comes first on the page, and then measure this
|
|
1842
|
+
// map against another viewer's rectangle — the overlap test below then
|
|
1843
|
+
// decides padding for a panel that may not overlap this map at all.
|
|
1844
|
+
const root = this._el.container.closest?.(".vco-storymap");
|
|
1845
|
+
// no viewer root means the map is standalone, so there is no slide
|
|
1846
|
+
// panel to make room for
|
|
1847
|
+
if (!root) return padding;
|
|
1848
|
+
const panel = root.querySelector(".vco-storyslider .vco-slide.vco-active .vco-text");
|
|
1008
1849
|
if (!panel) return padding;
|
|
1009
1850
|
const bg = getComputedStyle(panel).backgroundColor;
|
|
1010
1851
|
const match = /rgba?\(([^)]+)\)/.exec(bg);
|
|
@@ -1027,7 +1868,7 @@ export default class OpenLayers extends Map {
|
|
|
1027
1868
|
panel_rect.bottom > map_rect.top;
|
|
1028
1869
|
if (!overlaps) return padding;
|
|
1029
1870
|
const layout = this.options.layout;
|
|
1030
|
-
if (layout === "portrait") {
|
|
1871
|
+
if (layout === "portrait" || this.options.textmode === "bottom") {
|
|
1031
1872
|
padding[2] += Math.max(0, map_rect.bottom - panel_rect.top);
|
|
1032
1873
|
} else {
|
|
1033
1874
|
padding[1] += Math.max(0, panel_rect.right - map_rect.left);
|
|
@@ -1061,53 +1902,61 @@ export default class OpenLayers extends Map {
|
|
|
1061
1902
|
size: ol_map.getSize(),
|
|
1062
1903
|
padding: this._opaquePanelPadding(),
|
|
1063
1904
|
maxZoom: 12,
|
|
1064
|
-
duration: duration,
|
|
1065
|
-
easing: this.
|
|
1905
|
+
duration: prefersReducedMotion() ? 0 : duration,
|
|
1906
|
+
easing: this._easing,
|
|
1066
1907
|
});
|
|
1067
1908
|
}
|
|
1068
1909
|
|
|
1069
1910
|
_calculateMarkerZooms(): void {
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1911
|
+
const center = this._getMapCenter();
|
|
1912
|
+
for (const [i, marker] of this._markers.entries()) {
|
|
1913
|
+
// a marker with no location object at all is not navigable
|
|
1914
|
+
if (!marker.data.location) {
|
|
1915
|
+
continue;
|
|
1916
|
+
}
|
|
1917
|
+
let calculated_zoom: number | undefined;
|
|
1918
|
+
|
|
1919
|
+
// MARKER LOCATION — the overview fits around the current view
|
|
1920
|
+
// rather than a point on the map
|
|
1921
|
+
const marker_location =
|
|
1922
|
+
marker.data.type && marker.data.type === "overview" ? center : marker.location();
|
|
1923
|
+
if (!marker_location) {
|
|
1924
|
+
continue;
|
|
1925
|
+
}
|
|
1081
1926
|
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1927
|
+
// Fit-based zoom: zoom the view to this marker and neighbors
|
|
1928
|
+
const prev_marker = i > 0 ? this._markers[i - 1].location() : center;
|
|
1929
|
+
const next_marker =
|
|
1930
|
+
i < this._markers.length - 1 ? this._markers[i + 1].location() : center;
|
|
1931
|
+
|
|
1932
|
+
const prev_marker_zoom = prev_marker
|
|
1933
|
+
? this._calculateZoomChange(prev_marker, marker_location)
|
|
1934
|
+
: undefined;
|
|
1935
|
+
const next_marker_zoom = next_marker
|
|
1936
|
+
? this._calculateZoomChange(next_marker, marker_location)
|
|
1937
|
+
: undefined;
|
|
1938
|
+
|
|
1939
|
+
if (prev_marker_zoom && prev_marker_zoom < (next_marker_zoom ?? Infinity)) {
|
|
1940
|
+
calculated_zoom = prev_marker_zoom;
|
|
1941
|
+
} else if (next_marker_zoom) {
|
|
1942
|
+
calculated_zoom = next_marker_zoom;
|
|
1943
|
+
} else {
|
|
1944
|
+
calculated_zoom = prev_marker_zoom;
|
|
1945
|
+
}
|
|
1100
1946
|
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
this.options.map_center_offset.top !== 0)
|
|
1105
|
-
) {
|
|
1106
|
-
calculated_zoom = calculated_zoom - 1;
|
|
1107
|
-
}
|
|
1947
|
+
if (calculated_zoom === undefined) {
|
|
1948
|
+
continue;
|
|
1949
|
+
}
|
|
1108
1950
|
|
|
1109
|
-
|
|
1951
|
+
if (
|
|
1952
|
+
this.options.map_center_offset &&
|
|
1953
|
+
(this.options.map_center_offset.left !== 0 ||
|
|
1954
|
+
this.options.map_center_offset.top !== 0)
|
|
1955
|
+
) {
|
|
1956
|
+
calculated_zoom = calculated_zoom - 1;
|
|
1110
1957
|
}
|
|
1958
|
+
|
|
1959
|
+
this._marker_zooms[i] = calculated_zoom;
|
|
1111
1960
|
}
|
|
1112
1961
|
}
|
|
1113
1962
|
|
|
@@ -1149,25 +1998,32 @@ export default class OpenLayers extends Map {
|
|
|
1149
1998
|
this._map.addLayer(line);
|
|
1150
1999
|
}
|
|
1151
2000
|
|
|
1152
|
-
_addToLine(line: VectorLayer, d:
|
|
2001
|
+
_addToLine(line: VectorLayer, d: StorymapSlide): void {
|
|
1153
2002
|
// Append a point to the line's geometry
|
|
1154
2003
|
const source = line.getSource();
|
|
2004
|
+
if (!source) {
|
|
2005
|
+
return;
|
|
2006
|
+
}
|
|
1155
2007
|
let feature = source.getFeatures()[0];
|
|
1156
2008
|
if (!feature) {
|
|
1157
2009
|
feature = new Feature({ geometry: new LineString([]) });
|
|
1158
2010
|
source.addFeature(feature);
|
|
1159
2011
|
}
|
|
1160
2012
|
const coords = feature.getGeometry().getCoordinates();
|
|
1161
|
-
let lon = d.location
|
|
2013
|
+
let lon = d.location?.lon;
|
|
2014
|
+
const lat = d.location?.lat;
|
|
2015
|
+
if (lat === undefined || lon === undefined) {
|
|
2016
|
+
return;
|
|
2017
|
+
}
|
|
1162
2018
|
// unwrap dateline crossings against the previous point (issue #381).
|
|
1163
2019
|
// NB: read the previous longitude straight from meters — toLonLat()
|
|
1164
2020
|
// wraps into [-180, 180] and would collapse already-unwrapped values.
|
|
1165
|
-
if (coords.length > 0
|
|
2021
|
+
if (coords.length > 0) {
|
|
1166
2022
|
const last_lon = coords[coords.length - 1][0] / 111319.49079327358;
|
|
1167
2023
|
while (lon - last_lon > 180) lon -= 360;
|
|
1168
2024
|
while (lon - last_lon < -180) lon += 360;
|
|
1169
2025
|
}
|
|
1170
|
-
coords.push(this._toViewCoords({ lat
|
|
2026
|
+
coords.push(this._toViewCoords({ lat, lon }));
|
|
1171
2027
|
feature.getGeometry().setCoordinates(coords);
|
|
1172
2028
|
}
|
|
1173
2029
|
|
|
@@ -1198,6 +2054,9 @@ export default class OpenLayers extends Map {
|
|
|
1198
2054
|
const unwrapped = pts.map((p, i) => [lons[i], p[1]]);
|
|
1199
2055
|
const view_coords = this._markerCoordsToViewCoords(unwrapped);
|
|
1200
2056
|
const source = line.getSource();
|
|
2057
|
+
if (!source) {
|
|
2058
|
+
return;
|
|
2059
|
+
}
|
|
1201
2060
|
|
|
1202
2061
|
const setGeometry = (coords: number[][]) => {
|
|
1203
2062
|
source.clear();
|
|
@@ -1206,7 +2065,8 @@ export default class OpenLayers extends Map {
|
|
|
1206
2065
|
|
|
1207
2066
|
this._cancelLineAnimation();
|
|
1208
2067
|
|
|
1209
|
-
|
|
2068
|
+
// Reduced motion draws the full path at once instead of tracing it.
|
|
2069
|
+
const duration = prefersReducedMotion() ? 0 : (animate?.duration ?? 0);
|
|
1210
2070
|
if (duration <= 0 || view_coords.length < 2) {
|
|
1211
2071
|
setGeometry(view_coords);
|
|
1212
2072
|
return;
|
|
@@ -1267,7 +2127,7 @@ export default class OpenLayers extends Map {
|
|
|
1267
2127
|
const suffix = grow_len >= 1 ? view_coords.slice(grow_len - 1) : null;
|
|
1268
2128
|
const suffix_total = suffix ? this._pathLength(suffix) : 0;
|
|
1269
2129
|
|
|
1270
|
-
const easing = this.
|
|
2130
|
+
const easing = this._easing;
|
|
1271
2131
|
const start_time = performance.now();
|
|
1272
2132
|
if (prefix) {
|
|
1273
2133
|
// Seed the already-traveled prefix so it stays red from frame 0
|
|
@@ -1352,24 +2212,52 @@ export default class OpenLayers extends Map {
|
|
|
1352
2212
|
_panTo(loc: LatLngLiteral, animate?: boolean): void {
|
|
1353
2213
|
this._map.getView().animate({
|
|
1354
2214
|
center: this._toViewCoords(loc),
|
|
1355
|
-
duration: this.options.duration,
|
|
1356
|
-
easing: this.
|
|
2215
|
+
duration: prefersReducedMotion() ? 0 : this.options.duration,
|
|
2216
|
+
easing: this._easing,
|
|
1357
2217
|
});
|
|
1358
2218
|
}
|
|
1359
2219
|
|
|
1360
2220
|
_zoomTo(z: number, animate?: boolean): void {
|
|
1361
2221
|
this._map.getView().animate({
|
|
1362
2222
|
zoom: z,
|
|
1363
|
-
duration: this.options.duration,
|
|
1364
|
-
easing: this.
|
|
2223
|
+
duration: prefersReducedMotion() ? 0 : this.options.duration,
|
|
2224
|
+
easing: this._easing,
|
|
1365
2225
|
});
|
|
1366
2226
|
}
|
|
1367
2227
|
|
|
1368
2228
|
_viewTo(loc: StorymapSlideLocation, opts?: ViewToOptions): void {
|
|
2229
|
+
// Per-slide presentation first: basemap swap, filter grading and
|
|
2230
|
+
// spotlight mask are synchronous state, independent of movement.
|
|
2231
|
+
// Rotation joins the movement animation below (or _fitRegion's).
|
|
2232
|
+
// Slideshow tours only: internal documents move exactly as before.
|
|
2233
|
+
const feat = this.options.slideshow_source === true ? loc : undefined;
|
|
2234
|
+
this._switchSlideBasemap(feat);
|
|
2235
|
+
this._applySlideFilterAndMask(feat);
|
|
2236
|
+
// Image region stops (slideshow-style): in image mode the view is
|
|
2237
|
+
// EPSG:4326 where coordinates are raw image pixels — fit the xywh
|
|
2238
|
+
// region instead of flying to a point. Everything else (geo maps,
|
|
2239
|
+
// absent/invalid regions) keeps the point behavior unchanged.
|
|
2240
|
+
const is_image_space = this._map.getView().getProjection().getCode() === "EPSG:4326";
|
|
2241
|
+
const region =
|
|
2242
|
+
is_image_space && Array.isArray(loc.region) && loc.region.length === 4
|
|
2243
|
+
? loc.region
|
|
2244
|
+
: null;
|
|
2245
|
+
if (region) {
|
|
2246
|
+
this._fitRegion(region, opts, feat);
|
|
2247
|
+
return;
|
|
2248
|
+
}
|
|
2249
|
+
|
|
2250
|
+
const start = this._latLngOf(loc);
|
|
2251
|
+
if (!start) {
|
|
2252
|
+
// location-less slide (region-only on a geo map): nothing to
|
|
2253
|
+
// fly to, but an explicit rotation still applies
|
|
2254
|
+
this._applySlideRotation(feat, opts?.duration, opts?.duration === 0);
|
|
2255
|
+
return;
|
|
2256
|
+
}
|
|
1369
2257
|
let _animate = true,
|
|
1370
2258
|
_duration = this.options.duration,
|
|
1371
2259
|
_zoom = this._getMapZoom(),
|
|
1372
|
-
_location: LatLngLiteral =
|
|
2260
|
+
_location: LatLngLiteral = start;
|
|
1373
2261
|
|
|
1374
2262
|
// Show Active Line
|
|
1375
2263
|
if (!this.options.map_as_image) {
|
|
@@ -1400,15 +2288,25 @@ export default class OpenLayers extends Map {
|
|
|
1400
2288
|
_location = this._getMapCenterOffset(_location, _zoom);
|
|
1401
2289
|
}
|
|
1402
2290
|
|
|
2291
|
+
// A slide rotation joins the glide; the animation arg is only added
|
|
2292
|
+
// when the slide or the view is rotated, so unrotated stories
|
|
2293
|
+
// animate exactly as before.
|
|
2294
|
+
const _rotation = slideRotationRad(feat);
|
|
2295
|
+
const _current_rotation = this._map.getView().getRotation();
|
|
1403
2296
|
this._map.getView().animate({
|
|
1404
2297
|
center: this._toViewCoords(_location),
|
|
1405
2298
|
zoom: _zoom,
|
|
1406
|
-
|
|
1407
|
-
|
|
2299
|
+
...(_rotation !== null || _current_rotation !== 0 ? { rotation: _rotation ?? 0 } : {}),
|
|
2300
|
+
duration: _animate && !prefersReducedMotion() ? _duration : 0,
|
|
2301
|
+
easing: this._easing,
|
|
1408
2302
|
});
|
|
1409
2303
|
|
|
1410
2304
|
if (this._mini_map && this.options.width > this.options.skinny_size) {
|
|
1411
|
-
|
|
2305
|
+
// zoom_min_max stays null until the markers are measured (first
|
|
2306
|
+
// loadend), and `_zoom - 1 <= null` coerces to `<= 0` — the
|
|
2307
|
+
// minimap state was decided by an accidental coercion. Treat an
|
|
2308
|
+
// unmeasured range as "not zoomed out", i.e. keep it expanded.
|
|
2309
|
+
if (this.zoom_min_max.min !== null && _zoom - 1 <= this.zoom_min_max.min) {
|
|
1412
2310
|
this._mini_map.setCollapsed(true);
|
|
1413
2311
|
} else {
|
|
1414
2312
|
this._mini_map.setCollapsed(false);
|
|
@@ -1416,37 +2314,370 @@ export default class OpenLayers extends Map {
|
|
|
1416
2314
|
}
|
|
1417
2315
|
}
|
|
1418
2316
|
|
|
2317
|
+
/**
|
|
2318
|
+
* Image region stop: fit the xywh bbox ([x, y, w, h] image pixels —
|
|
2319
|
+
* the EPSG:4326 image space) with the shared animation options, then
|
|
2320
|
+
* keep the minimap collapse state in sync like `_viewTo`. A slide
|
|
2321
|
+
* rotation follows the fit (fit() takes no rotation), plus the
|
|
2322
|
+
* filter/mask tail when the location is passed.
|
|
2323
|
+
*/
|
|
2324
|
+
_fitRegion(
|
|
2325
|
+
region: [number, number, number, number],
|
|
2326
|
+
opts?: ViewToOptions,
|
|
2327
|
+
loc?: StorymapSlideLocation,
|
|
2328
|
+
): void {
|
|
2329
|
+
const [x, y, w, h] = region;
|
|
2330
|
+
if (!(w > 0) || !(h > 0)) {
|
|
2331
|
+
return;
|
|
2332
|
+
}
|
|
2333
|
+
let _animate = true;
|
|
2334
|
+
let _duration = this.options.duration;
|
|
2335
|
+
if (opts && opts.duration !== undefined) {
|
|
2336
|
+
if (opts.duration === 0) {
|
|
2337
|
+
_animate = false;
|
|
2338
|
+
} else {
|
|
2339
|
+
_duration = opts.duration;
|
|
2340
|
+
}
|
|
2341
|
+
}
|
|
2342
|
+
this._map.getView().fit([x, y, x + w, y + h], {
|
|
2343
|
+
size: this._map.getSize(),
|
|
2344
|
+
padding: this._opaquePanelPadding(),
|
|
2345
|
+
duration: _animate && !prefersReducedMotion() ? _duration : 0,
|
|
2346
|
+
easing: this._easing,
|
|
2347
|
+
});
|
|
2348
|
+
if (loc) {
|
|
2349
|
+
this._applySlideRotation(loc, _duration, !_animate);
|
|
2350
|
+
this._applySlideFilterAndMask(loc);
|
|
2351
|
+
}
|
|
2352
|
+
if (this._mini_map && this.options.width > this.options.skinny_size) {
|
|
2353
|
+
this._mini_map.setCollapsed(true);
|
|
2354
|
+
}
|
|
2355
|
+
}
|
|
2356
|
+
|
|
1419
2357
|
_toViewCoords(loc: LatLngLiteral): number[] {
|
|
1420
2358
|
const is_image_space = this._map.getView().getProjection().getCode() === "EPSG:4326";
|
|
1421
2359
|
if (is_image_space) return [loc.lon, loc.lat];
|
|
1422
2360
|
return fromLonLat([loc.lon, loc.lat]);
|
|
1423
2361
|
}
|
|
1424
2362
|
|
|
2363
|
+
/* Per-slide view presentation (slideshow rotation/filter/mask/basemap)
|
|
2364
|
+
================================================== */
|
|
2365
|
+
/**
|
|
2366
|
+
* Swap the base layer for a slide `location.basemap` (multi-manifest
|
|
2367
|
+
* tours): built on first use through the tile layer factory (custom
|
|
2368
|
+
* factories apply), then cached. A slide without one restores the story
|
|
2369
|
+
* basemap. No-op when the requested key is already showing, so resize
|
|
2370
|
+
* and re-fit paths pass through untouched. Consent-denied tiles skip
|
|
2371
|
+
* the switch entirely.
|
|
2372
|
+
*/
|
|
2373
|
+
_switchSlideBasemap(loc: StorymapSlideLocation | null | undefined): void {
|
|
2374
|
+
// slideshow tours only: internal documents keep the story basemap
|
|
2375
|
+
if (this.options.slideshow_source !== true) return;
|
|
2376
|
+
if (!this._tilesAllowed()) return;
|
|
2377
|
+
const key = slideBasemapKey(loc);
|
|
2378
|
+
if (key === this._slide_basemap) return;
|
|
2379
|
+
if (this._tile_layer) {
|
|
2380
|
+
this._map.removeLayer(this._tile_layer);
|
|
2381
|
+
}
|
|
2382
|
+
if (key === null) {
|
|
2383
|
+
this._tile_layer =
|
|
2384
|
+
this._story_basemap_layer ?? this._createTileLayer(this.options.map_type);
|
|
2385
|
+
this._story_basemap_layer = this._tile_layer;
|
|
2386
|
+
} else {
|
|
2387
|
+
let layer = this._basemap_cache.get(key);
|
|
2388
|
+
if (!layer) {
|
|
2389
|
+
layer = this._createSlideBasemap(key);
|
|
2390
|
+
layer.setZIndex(0);
|
|
2391
|
+
this._basemap_cache.set(key, layer);
|
|
2392
|
+
}
|
|
2393
|
+
this._tile_layer = layer;
|
|
2394
|
+
}
|
|
2395
|
+
this._tile_layer.setZIndex(0);
|
|
2396
|
+
this._map.addLayer(this._tile_layer);
|
|
2397
|
+
this._slide_basemap = key;
|
|
2398
|
+
this._updateAttribution();
|
|
2399
|
+
}
|
|
2400
|
+
|
|
2401
|
+
/**
|
|
2402
|
+
* Build a per-slide basemap layer: IIIF `info.json` URLs (or bare
|
|
2403
|
+
* service bases) become tile layers; plain image URLs become static
|
|
2404
|
+
* image layers (dimensions probed); anything else goes through the
|
|
2405
|
+
* regular tile layer factory, so keywords and templates keep working.
|
|
2406
|
+
*/
|
|
2407
|
+
_createSlideBasemap(key: string): Layer {
|
|
2408
|
+
// plain image files (absolute or same-origin relative) skip the
|
|
2409
|
+
// doomed info.json fetch; services and keywords flow on
|
|
2410
|
+
if (isStaticImageUrl(key)) {
|
|
2411
|
+
return this._createStaticImageBasemap(key);
|
|
2412
|
+
}
|
|
2413
|
+
if (/^https?:\/\//i.test(key) && !key.includes("{z}")) {
|
|
2414
|
+
const info_url = serviceToInfoJson(key);
|
|
2415
|
+
const pending: TileLayer = new TileLayer();
|
|
2416
|
+
fetch(info_url)
|
|
2417
|
+
.then((r) => r.json())
|
|
2418
|
+
.then((info: unknown) => {
|
|
2419
|
+
if (this._disposed) return;
|
|
2420
|
+
const parsed = new IIIFInfo(
|
|
2421
|
+
info as ImageInformationResponse,
|
|
2422
|
+
).getTileSourceOptions();
|
|
2423
|
+
const dims = info as { width?: number; height?: number };
|
|
2424
|
+
if (typeof dims.width !== "number" || typeof dims.height !== "number") {
|
|
2425
|
+
this._replaceWithStaticBasemap(key, pending);
|
|
2426
|
+
return;
|
|
2427
|
+
}
|
|
2428
|
+
const source = new IIIF({
|
|
2429
|
+
...(parsed ?? {}),
|
|
2430
|
+
projection: "EPSG:4326",
|
|
2431
|
+
size: [dims.width, dims.height],
|
|
2432
|
+
crossOrigin: "anonymous",
|
|
2433
|
+
});
|
|
2434
|
+
pending.setSource(source);
|
|
2435
|
+
this._fireImageready(source, "iiif", pending);
|
|
2436
|
+
})
|
|
2437
|
+
.catch(() => {
|
|
2438
|
+
// not an image service after all (a plain file, e.g. a
|
|
2439
|
+
// `.jpx` that serves no info.json): probe it as one
|
|
2440
|
+
this._replaceWithStaticBasemap(key, pending);
|
|
2441
|
+
});
|
|
2442
|
+
return pending;
|
|
2443
|
+
}
|
|
2444
|
+
return this._createTileLayer(key);
|
|
2445
|
+
}
|
|
2446
|
+
|
|
2447
|
+
/**
|
|
2448
|
+
* A basemap from a plain image URL (no IIIF service): an `ImageLayer`
|
|
2449
|
+
* filled in once the natural size probes. Used for per-slide basemaps
|
|
2450
|
+
* pointing at static files, and as the fallback when an `info.json`
|
|
2451
|
+
* turns out unreachable (some `.jpx`/`.jp2` bases serve one, some are
|
|
2452
|
+
* plain files — only the fetch tells). Unusable dimensions keep the
|
|
2453
|
+
* error logged and the layer empty, like the failed-service path.
|
|
2454
|
+
*/
|
|
2455
|
+
_createStaticImageBasemap(url: string): Layer {
|
|
2456
|
+
const pending = new ImageLayer();
|
|
2457
|
+
pending.setZIndex(0);
|
|
2458
|
+
const token = (this._slide_probe_tokens.get(url) ?? 0) + 1;
|
|
2459
|
+
this._slide_probe_tokens.set(url, token);
|
|
2460
|
+
void this._probeStaticImage(url).then(
|
|
2461
|
+
(dims) => {
|
|
2462
|
+
if (this._disposed || this._slide_probe_tokens.get(url) !== token) return;
|
|
2463
|
+
if (!dims || !(dims.width > 0) || !(dims.height > 0)) {
|
|
2464
|
+
console.error("Slide basemap image has no usable dimensions:", url);
|
|
2465
|
+
return;
|
|
2466
|
+
}
|
|
2467
|
+
pending.setSource(
|
|
2468
|
+
this._newStaticSource(url, [0, 0, dims.width, dims.height], "EPSG:4326"),
|
|
2469
|
+
);
|
|
2470
|
+
this._fireImageready(
|
|
2471
|
+
pending.getSource() as { getState?(): string },
|
|
2472
|
+
"iiif",
|
|
2473
|
+
pending,
|
|
2474
|
+
);
|
|
2475
|
+
},
|
|
2476
|
+
(err) =>
|
|
2477
|
+
console.error(
|
|
2478
|
+
"Slide basemap image could not be probed:",
|
|
2479
|
+
url,
|
|
2480
|
+
(err as Error)?.stack || err,
|
|
2481
|
+
),
|
|
2482
|
+
);
|
|
2483
|
+
return pending;
|
|
2484
|
+
}
|
|
2485
|
+
|
|
2486
|
+
/**
|
|
2487
|
+
* Replace a failed IIIF slide basemap with a probed static image under
|
|
2488
|
+
* the same cache key, swapping it into the map when still current (the
|
|
2489
|
+
* visitor may have moved on while the fetch was in flight, in which
|
|
2490
|
+
* case the cache holds it for a return visit).
|
|
2491
|
+
*/
|
|
2492
|
+
_replaceWithStaticBasemap(key: string, pending: TileLayer): void {
|
|
2493
|
+
if (this._disposed) return;
|
|
2494
|
+
const staticLayer = this._createStaticImageBasemap(key);
|
|
2495
|
+
this._basemap_cache.set(key, staticLayer);
|
|
2496
|
+
if (this._tile_layer === (pending as unknown as Layer)) {
|
|
2497
|
+
this._map.removeLayer(pending);
|
|
2498
|
+
this._tile_layer = staticLayer;
|
|
2499
|
+
this._map.addLayer(staticLayer);
|
|
2500
|
+
}
|
|
2501
|
+
}
|
|
2502
|
+
|
|
2503
|
+
/**
|
|
2504
|
+
* The story's own base layer (as opposed to a per-slide one): stashed
|
|
2505
|
+
* on creation so a slide basemap can hand back to it without refetching.
|
|
2506
|
+
*/
|
|
2507
|
+
declare "_story_basemap_layer": Layer | null;
|
|
2508
|
+
|
|
2509
|
+
/**
|
|
2510
|
+
* Apply a slide's CSS filter grading to the map viewport
|
|
2511
|
+
* (slideshow `filters`). Always assigned — "" clears — so grading never
|
|
2512
|
+
* leaks onto the next slide.
|
|
2513
|
+
*/
|
|
2514
|
+
_applySlideFilter(loc: StorymapSlideLocation | null | undefined): void {
|
|
2515
|
+
this._map.getViewport().style.filter = buildSlideFilter(loc?.filter);
|
|
2516
|
+
}
|
|
2517
|
+
|
|
2518
|
+
/**
|
|
2519
|
+
* Apply a slide's spotlight mask (slideshow `passepartout`): normalized
|
|
2520
|
+
* viewport fractions stay visible while the rest is dimmed. Rendered as
|
|
2521
|
+
* up to four percentage-positioned divs (or one for `invert`), so it
|
|
2522
|
+
* needs no updates on pan/zoom/resize. `pointer-events: none` keeps
|
|
2523
|
+
* markers clickable through the shade; the marker overlay container
|
|
2524
|
+
* (z-index 1) paints above it. Absent/invalid clears the mask.
|
|
2525
|
+
*/
|
|
2526
|
+
_applySlideMask(loc: StorymapSlideLocation | null | undefined): void {
|
|
2527
|
+
const mask: NormalizedMask | null = normalizeMask(loc?.mask);
|
|
2528
|
+
if (!mask) {
|
|
2529
|
+
this._mask_el?.remove();
|
|
2530
|
+
this._mask_el = null;
|
|
2531
|
+
return;
|
|
2532
|
+
}
|
|
2533
|
+
const viewport = this._map.getViewport();
|
|
2534
|
+
if (!this._mask_el) {
|
|
2535
|
+
const el = document.createElement("div");
|
|
2536
|
+
el.className = "vco-map-mask";
|
|
2537
|
+
el.setAttribute("aria-hidden", "true");
|
|
2538
|
+
el.style.position = "absolute";
|
|
2539
|
+
el.style.inset = "0";
|
|
2540
|
+
el.style.zIndex = "0";
|
|
2541
|
+
el.style.pointerEvents = "none";
|
|
2542
|
+
el.style.overflow = "hidden";
|
|
2543
|
+
viewport.appendChild(el);
|
|
2544
|
+
this._mask_el = el;
|
|
2545
|
+
}
|
|
2546
|
+
const pct = (n: number) => `${n * 100}%`;
|
|
2547
|
+
const shade = (left: string, top: string, width: string, height: string) =>
|
|
2548
|
+
`<div style="position:absolute;left:${left};top:${top};width:${width};height:${height};background:${mask.color};opacity:${mask.opacity}"></div>`;
|
|
2549
|
+
this._mask_el.innerHTML = mask.invert
|
|
2550
|
+
? shade(pct(mask.x), pct(mask.y), pct(mask.w), pct(mask.h))
|
|
2551
|
+
: shade("0", "0", "100%", pct(mask.y)) +
|
|
2552
|
+
shade("0", pct(mask.y + mask.h), "100%", pct(1 - mask.y - mask.h)) +
|
|
2553
|
+
shade("0", pct(mask.y), pct(mask.x), pct(mask.h)) +
|
|
2554
|
+
shade(pct(mask.x + mask.w), pct(mask.y), pct(1 - mask.x - mask.w), pct(mask.h));
|
|
2555
|
+
}
|
|
2556
|
+
|
|
2557
|
+
/** Filter + mask together: the synchronous tail of every navigation. */
|
|
2558
|
+
_applySlideFilterAndMask(loc: StorymapSlideLocation | null | undefined): void {
|
|
2559
|
+
this._applySlideFilter(loc);
|
|
2560
|
+
this._applySlideMask(loc);
|
|
2561
|
+
}
|
|
2562
|
+
|
|
2563
|
+
/**
|
|
2564
|
+
* Rotate the view to a slide `rotation` (degrees clockwise). Merges into
|
|
2565
|
+
* the caller's animation when one is running; on its own (region fits,
|
|
2566
|
+
* location-less slides, overview reset) it animates alone, or sets
|
|
2567
|
+
* synchronously when the navigation is instant. A no-op when neither
|
|
2568
|
+
* the slide nor the view is rotated, so unrotated stories animate
|
|
2569
|
+
* exactly as before.
|
|
2570
|
+
*/
|
|
2571
|
+
_applySlideRotation(
|
|
2572
|
+
loc: StorymapSlideLocation | null | undefined,
|
|
2573
|
+
duration?: number,
|
|
2574
|
+
instant = false,
|
|
2575
|
+
): void {
|
|
2576
|
+
const rotation = slideRotationRad(loc);
|
|
2577
|
+
const view = this._map.getView();
|
|
2578
|
+
if (rotation === null && view.getRotation() === 0) return;
|
|
2579
|
+
const target = rotation ?? 0;
|
|
2580
|
+
if (instant || prefersReducedMotion()) {
|
|
2581
|
+
view.setRotation(target);
|
|
2582
|
+
return;
|
|
2583
|
+
}
|
|
2584
|
+
view.animate({
|
|
2585
|
+
rotation: target,
|
|
2586
|
+
duration: duration ?? this.options.duration,
|
|
2587
|
+
easing: this._easing,
|
|
2588
|
+
});
|
|
2589
|
+
}
|
|
2590
|
+
|
|
2591
|
+
/**
|
|
2592
|
+
* Show a per-slide image overlay (slideshow `imgoverlay`) while its
|
|
2593
|
+
* slide is active. `extent` pins it (`[west, south, east, north]` lon/lat,
|
|
2594
|
+
* raw image pixels on image-space maps); otherwise it covers the current
|
|
2595
|
+
* view, optionally scaled to `size` (fraction of the viewport). Painted
|
|
2596
|
+
* above the base (z 5) below the route lines. Null/empty clears.
|
|
2597
|
+
* Consent-denied tiles skip it like every other layer.
|
|
2598
|
+
*/
|
|
2599
|
+
setSlideImageOverlay(overlay: StorymapImageOverlay | null | undefined): void {
|
|
2600
|
+
if (this._imgoverlay_layer) {
|
|
2601
|
+
this._map.removeLayer(this._imgoverlay_layer);
|
|
2602
|
+
this._imgoverlay_layer = null;
|
|
2603
|
+
}
|
|
2604
|
+
if (!overlay || typeof overlay.url !== "string" || overlay.url === "") return;
|
|
2605
|
+
if (!this._tilesAllowed()) return;
|
|
2606
|
+
// slideshow tours only: internal documents never paint overlays
|
|
2607
|
+
if (this.options.slideshow_source !== true) return;
|
|
2608
|
+
const given = overlay.extent;
|
|
2609
|
+
let extent: Extent;
|
|
2610
|
+
if (
|
|
2611
|
+
Array.isArray(given) &&
|
|
2612
|
+
given.length === 4 &&
|
|
2613
|
+
given.every((n) => typeof n === "number" && Number.isFinite(n))
|
|
2614
|
+
) {
|
|
2615
|
+
const raw = (
|
|
2616
|
+
this.isImageSpace()
|
|
2617
|
+
? [given[0], given[1], given[2], given[3]]
|
|
2618
|
+
: [...fromLonLat([given[0], given[1]]), ...fromLonLat([given[2], given[3]])]
|
|
2619
|
+
) as Extent;
|
|
2620
|
+
// lon/lat corners can arrive swapped; the view fit needs min/max
|
|
2621
|
+
extent = [
|
|
2622
|
+
Math.min(raw[0], raw[2]),
|
|
2623
|
+
Math.min(raw[1], raw[3]),
|
|
2624
|
+
Math.max(raw[0], raw[2]),
|
|
2625
|
+
Math.max(raw[1], raw[3]),
|
|
2626
|
+
];
|
|
2627
|
+
} else {
|
|
2628
|
+
extent = this._map.getView().calculateExtent(this._viewportSize());
|
|
2629
|
+
if (typeof overlay.size === "number" && Number.isFinite(overlay.size)) {
|
|
2630
|
+
const scale = Math.min(1, Math.max(0.05, overlay.size));
|
|
2631
|
+
const cx = (extent[0] + extent[2]) / 2;
|
|
2632
|
+
const cy = (extent[1] + extent[3]) / 2;
|
|
2633
|
+
const hw = ((extent[2] - extent[0]) * scale) / 2;
|
|
2634
|
+
const hh = ((extent[3] - extent[1]) * scale) / 2;
|
|
2635
|
+
extent = [cx - hw, cy - hh, cx + hw, cy + hh];
|
|
2636
|
+
}
|
|
2637
|
+
}
|
|
2638
|
+
const opacity =
|
|
2639
|
+
typeof overlay.opacity === "number" && Number.isFinite(overlay.opacity)
|
|
2640
|
+
? Math.min(1, Math.max(0, overlay.opacity))
|
|
2641
|
+
: 1;
|
|
2642
|
+
const layer = this._newStaticLayer(
|
|
2643
|
+
overlay.url,
|
|
2644
|
+
extent,
|
|
2645
|
+
this._map.getView().getProjection(),
|
|
2646
|
+
{ opacity, zIndex: 5 },
|
|
2647
|
+
);
|
|
2648
|
+
this._map.addLayer(layer);
|
|
2649
|
+
this._imgoverlay_layer = layer;
|
|
2650
|
+
}
|
|
2651
|
+
|
|
1425
2652
|
_getMapZoom(): number {
|
|
1426
2653
|
// fractional zoom on purpose: overview fits produce non-integer zooms and
|
|
1427
2654
|
// rounding here would snap the view on the next navigation
|
|
1428
2655
|
return this._map.getView().getZoom() || 0;
|
|
1429
2656
|
}
|
|
1430
2657
|
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
);
|
|
1436
|
-
|
|
2658
|
+
// Note: there is deliberately no offset-aware variant. The exposed
|
|
2659
|
+
// center is panel-shifted (see the migration guide); a previous
|
|
2660
|
+
// `offset?: boolean` parameter promised a correction it never applied.
|
|
2661
|
+
_getMapCenter(): LatLngLiteral {
|
|
2662
|
+
const view = this._map.getView();
|
|
2663
|
+
const center = view.getCenter() ?? [0, 0];
|
|
2664
|
+
const [lon, lat] = toLonLat(center, view.getProjection());
|
|
2665
|
+
return { lat: lat, lon: lon };
|
|
1437
2666
|
}
|
|
1438
2667
|
|
|
1439
2668
|
_getMapCenterOffset(location: LatLngLiteral, zoom: number): LatLngLiteral {
|
|
1440
|
-
// Offset the center by map_center_offset pixels at the given zoom
|
|
2669
|
+
// Offset the center by map_center_offset pixels at the given zoom.
|
|
2670
|
+
// A null map_center_offset means "no offset" (the default).
|
|
2671
|
+
const offset = this.options.map_center_offset;
|
|
2672
|
+
if (!offset || (offset.left === 0 && offset.top === 0)) {
|
|
2673
|
+
return location;
|
|
2674
|
+
}
|
|
1441
2675
|
const view = this._map.getView();
|
|
1442
2676
|
const projection = view.getProjection();
|
|
1443
2677
|
const center = this._toViewCoords(location);
|
|
1444
2678
|
const resolution = view.getResolutionForZoom(zoom);
|
|
1445
2679
|
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
|
-
],
|
|
2680
|
+
[center[0] - offset.left * resolution, center[1] + offset.top * resolution],
|
|
1450
2681
|
projection,
|
|
1451
2682
|
);
|
|
1452
2683
|
}
|
|
@@ -1459,13 +2690,48 @@ export default class OpenLayers extends Map {
|
|
|
1459
2690
|
return { lat: c[1], lon: (coord[0] / 6378137) * (180 / Math.PI) };
|
|
1460
2691
|
}
|
|
1461
2692
|
|
|
2693
|
+
/**
|
|
2694
|
+
* May a tile-backed layer (base map, overlay, minimap, overview) be
|
|
2695
|
+
* created right now? Under `consent_required` that is only true once the
|
|
2696
|
+
* visitor has allowed the tile service. This was the same five-line
|
|
2697
|
+
* predicate at five call sites.
|
|
2698
|
+
*/
|
|
2699
|
+
private _tilesAllowed(): boolean {
|
|
2700
|
+
const consent = consentManagerOf(this.options);
|
|
2701
|
+
if (!this.options.consent_required || !consent) {
|
|
2702
|
+
return true;
|
|
2703
|
+
}
|
|
2704
|
+
return consent.isGranted(tileService().key);
|
|
2705
|
+
}
|
|
2706
|
+
|
|
2707
|
+
/** A slide's {lat, lon} when both are real numbers, else null. */
|
|
2708
|
+
private _latLngOf(loc: StorymapSlideLocation | null | undefined): LatLngLiteral | null {
|
|
2709
|
+
if (!loc || typeof loc.lat !== "number" || typeof loc.lon !== "number") {
|
|
2710
|
+
return null;
|
|
2711
|
+
}
|
|
2712
|
+
return { lat: loc.lat, lon: loc.lon };
|
|
2713
|
+
}
|
|
2714
|
+
|
|
2715
|
+
/**
|
|
2716
|
+
* The map's pixel size, with a never-zero fallback.
|
|
2717
|
+
*
|
|
2718
|
+
* `getSize()` returns undefined until the map has a size, so the six
|
|
2719
|
+
* call sites that fitted an extent to the viewport all repeated the same
|
|
2720
|
+
* `(size[0] || 1)` guard — which silently fitted against a 1px viewport
|
|
2721
|
+
* rather than bailing out.
|
|
2722
|
+
*/
|
|
2723
|
+
private _viewportSize(): [number, number] {
|
|
2724
|
+
const size = this._map?.getSize();
|
|
2725
|
+
return [size?.[0] || 1, size?.[1] || 1];
|
|
2726
|
+
}
|
|
2727
|
+
|
|
1462
2728
|
_getBoundsZoom(
|
|
1463
2729
|
origin: LatLngLiteral,
|
|
1464
2730
|
destination: LatLngLiteral,
|
|
1465
2731
|
correct_for_center?: boolean,
|
|
1466
2732
|
): number {
|
|
1467
2733
|
const coords = [
|
|
1468
|
-
[origin.lon
|
|
2734
|
+
[origin.lon, origin.lat],
|
|
1469
2735
|
[destination.lon, destination.lat],
|
|
1470
2736
|
];
|
|
1471
2737
|
const view_coords = this._markerCoordsToViewCoords(coords);
|
|
@@ -1518,6 +2784,25 @@ export default class OpenLayers extends Map {
|
|
|
1518
2784
|
this._cancelLineAnimation();
|
|
1519
2785
|
this._line_active.setVisible(false);
|
|
1520
2786
|
|
|
2787
|
+
// The overview shows the whole story: per-slide grading, mask and
|
|
2788
|
+
// rotation do not apply. Each reset is guarded so the common path
|
|
2789
|
+
// (nothing set) touches nothing.
|
|
2790
|
+
if (this._map.getView().getRotation() !== 0) {
|
|
2791
|
+
this._map.getView().animate({
|
|
2792
|
+
rotation: 0,
|
|
2793
|
+
duration: prefersReducedMotion() ? 0 : (duration ?? this._transition_duration),
|
|
2794
|
+
easing: this._easing,
|
|
2795
|
+
});
|
|
2796
|
+
}
|
|
2797
|
+
if (this._map.getViewport().style.filter !== "") {
|
|
2798
|
+
this._map.getViewport().style.filter = "";
|
|
2799
|
+
}
|
|
2800
|
+
if (this._mask_el) {
|
|
2801
|
+
this._mask_el.remove();
|
|
2802
|
+
this._mask_el = null;
|
|
2803
|
+
}
|
|
2804
|
+
this._switchSlideBasemap(null);
|
|
2805
|
+
|
|
1521
2806
|
// repeated presses while an overview animation is running snap
|
|
1522
2807
|
// instantly instead of restarting the ~1s animation
|
|
1523
2808
|
if (duration && this._map.getView().getAnimating()) {
|
|
@@ -1548,29 +2833,51 @@ export default class OpenLayers extends Map {
|
|
|
1548
2833
|
this._map.getView().animate({
|
|
1549
2834
|
center: center_view,
|
|
1550
2835
|
zoom: view_zoom,
|
|
1551
|
-
duration: duration ?? this._transition_duration,
|
|
1552
|
-
easing: this.
|
|
2836
|
+
duration: prefersReducedMotion() ? 0 : (duration ?? this._transition_duration),
|
|
2837
|
+
easing: this._easing,
|
|
1553
2838
|
});
|
|
1554
2839
|
}
|
|
1555
|
-
} else if (this.
|
|
1556
|
-
const source = this._tile_layer
|
|
2840
|
+
} else if (this.isImageSpace()) {
|
|
2841
|
+
const source = this._sourceOf(this._tile_layer);
|
|
1557
2842
|
if (!source) {
|
|
1558
2843
|
return;
|
|
1559
2844
|
}
|
|
1560
2845
|
const fit = () => {
|
|
1561
2846
|
try {
|
|
1562
|
-
|
|
2847
|
+
// static fallback (no tile grid): fit the single image
|
|
2848
|
+
// extent directly so the overview still frames the picture
|
|
2849
|
+
const static_extent = (
|
|
2850
|
+
source as { getImageExtent?: () => Extent | null }
|
|
2851
|
+
).getImageExtent?.();
|
|
2852
|
+
if (static_extent) {
|
|
2853
|
+
this._map.getView().fit(static_extent, {
|
|
2854
|
+
size: this._viewportSize(),
|
|
2855
|
+
padding: this._opaquePanelPadding(),
|
|
2856
|
+
duration: prefersReducedMotion()
|
|
2857
|
+
? 0
|
|
2858
|
+
: (duration ?? this._transition_duration),
|
|
2859
|
+
easing: this._easing,
|
|
2860
|
+
});
|
|
2861
|
+
return;
|
|
2862
|
+
}
|
|
2863
|
+
// a custom layer with a source is not necessarily a tile
|
|
2864
|
+
// source; only tile grids can supply a fit extent
|
|
2865
|
+
const grid = (
|
|
2866
|
+
source as {
|
|
2867
|
+
getTileGrid?: () => { getExtent(): number[] } | null;
|
|
2868
|
+
}
|
|
2869
|
+
).getTileGrid?.();
|
|
1563
2870
|
if (grid) {
|
|
1564
2871
|
// compute the fit target directly and animate once (a
|
|
1565
2872
|
// fit() followed by setCenter() would cancel the fit
|
|
1566
2873
|
// animation, issue #465)
|
|
1567
2874
|
const extent = grid.getExtent();
|
|
1568
|
-
const size = this.
|
|
1569
|
-
const resolution_x = (extent[2] - extent[0]) /
|
|
1570
|
-
const resolution_y = (extent[3] - extent[1]) /
|
|
2875
|
+
const size = this._viewportSize();
|
|
2876
|
+
const resolution_x = (extent[2] - extent[0]) / size[0];
|
|
2877
|
+
const resolution_y = (extent[3] - extent[1]) / size[1];
|
|
1571
2878
|
const resolution = Math.max(resolution_x, resolution_y);
|
|
1572
2879
|
const view = this._map.getView();
|
|
1573
|
-
const zoom = view.getZoomForResolution(resolution);
|
|
2880
|
+
const zoom = view.getZoomForResolution(resolution) ?? 0;
|
|
1574
2881
|
const overview_center = this.options.map_overview_center;
|
|
1575
2882
|
const center_px = overview_center
|
|
1576
2883
|
? this._toViewCoords({
|
|
@@ -1584,8 +2891,10 @@ export default class OpenLayers extends Map {
|
|
|
1584
2891
|
view.animate({
|
|
1585
2892
|
center: this._toViewCoords(offset_location),
|
|
1586
2893
|
zoom: zoom,
|
|
1587
|
-
duration:
|
|
1588
|
-
|
|
2894
|
+
duration: prefersReducedMotion()
|
|
2895
|
+
? 0
|
|
2896
|
+
: (duration ?? this._transition_duration),
|
|
2897
|
+
easing: this._easing,
|
|
1589
2898
|
});
|
|
1590
2899
|
}
|
|
1591
2900
|
} catch (e) {
|
|
@@ -1609,14 +2918,14 @@ export default class OpenLayers extends Map {
|
|
|
1609
2918
|
if (overview_center && this.bounds_array && this.bounds_array.length) {
|
|
1610
2919
|
const view_coords = this._markerCoordsToViewCoords(this.bounds_array);
|
|
1611
2920
|
const extent = boundingExtent(view_coords);
|
|
1612
|
-
const size = this.
|
|
2921
|
+
const size = this._viewportSize();
|
|
1613
2922
|
const resolution = Math.max(
|
|
1614
|
-
(extent[2] - extent[0]) /
|
|
1615
|
-
(extent[3] - extent[1]) /
|
|
2923
|
+
(extent[2] - extent[0]) / size[0],
|
|
2924
|
+
(extent[3] - extent[1]) / size[1],
|
|
1616
2925
|
);
|
|
1617
2926
|
const zoom = Math.max(
|
|
1618
2927
|
0,
|
|
1619
|
-
Math.round(this._map.getView().getZoomForResolution(resolution)) - 1,
|
|
2928
|
+
Math.round(this._map.getView().getZoomForResolution(resolution) ?? 0) - 1,
|
|
1620
2929
|
);
|
|
1621
2930
|
const offset_location = this._getMapCenterOffset(
|
|
1622
2931
|
{ lat: overview_center.lat, lon: overview_center.lon },
|
|
@@ -1625,8 +2934,8 @@ export default class OpenLayers extends Map {
|
|
|
1625
2934
|
this._map.getView().animate({
|
|
1626
2935
|
center: this._toViewCoords(offset_location),
|
|
1627
2936
|
zoom: zoom,
|
|
1628
|
-
duration: duration ?? this._transition_duration,
|
|
1629
|
-
easing: this.
|
|
2937
|
+
duration: prefersReducedMotion() ? 0 : (duration ?? this._transition_duration),
|
|
2938
|
+
easing: this._easing,
|
|
1630
2939
|
});
|
|
1631
2940
|
} else if (
|
|
1632
2941
|
this.options.map_center_offset &&
|
|
@@ -1636,13 +2945,13 @@ export default class OpenLayers extends Map {
|
|
|
1636
2945
|
if (this.bounds_array && this.bounds_array.length) {
|
|
1637
2946
|
const view_coords = this._markerCoordsToViewCoords(this.bounds_array);
|
|
1638
2947
|
const extent = boundingExtent(view_coords);
|
|
1639
|
-
const size = this.
|
|
1640
|
-
const resolution_x = (extent[2] - extent[0]) /
|
|
1641
|
-
const resolution_y = (extent[3] - extent[1]) /
|
|
2948
|
+
const size = this._viewportSize();
|
|
2949
|
+
const resolution_x = (extent[2] - extent[0]) / size[0];
|
|
2950
|
+
const resolution_y = (extent[3] - extent[1]) / size[1];
|
|
1642
2951
|
const resolution = Math.max(resolution_x, resolution_y);
|
|
1643
2952
|
const zoom = Math.max(
|
|
1644
2953
|
0,
|
|
1645
|
-
Math.round(this._map.getView().getZoomForResolution(resolution)) - 1,
|
|
2954
|
+
Math.round(this._map.getView().getZoomForResolution(resolution) ?? 0) - 1,
|
|
1646
2955
|
);
|
|
1647
2956
|
const center_px = [(extent[0] + extent[2]) / 2, (extent[1] + extent[3]) / 2];
|
|
1648
2957
|
const projection = this._map.getView().getProjection();
|
|
@@ -1651,8 +2960,10 @@ export default class OpenLayers extends Map {
|
|
|
1651
2960
|
this._map.getView().animate({
|
|
1652
2961
|
center: this._toViewCoords(offset_location),
|
|
1653
2962
|
zoom: zoom,
|
|
1654
|
-
duration:
|
|
1655
|
-
|
|
2963
|
+
duration: prefersReducedMotion()
|
|
2964
|
+
? 0
|
|
2965
|
+
: (duration ?? this._transition_duration),
|
|
2966
|
+
easing: this._easing,
|
|
1656
2967
|
});
|
|
1657
2968
|
}
|
|
1658
2969
|
} else {
|
|
@@ -1664,13 +2975,18 @@ export default class OpenLayers extends Map {
|
|
|
1664
2975
|
const view = this._map.getView();
|
|
1665
2976
|
const zoom = view.getZoom();
|
|
1666
2977
|
if (zoom !== undefined) {
|
|
1667
|
-
const center = this._fromViewCoords(
|
|
2978
|
+
const center = this._fromViewCoords(
|
|
2979
|
+
view.getCenter() ?? [0, 0],
|
|
2980
|
+
view.getProjection(),
|
|
2981
|
+
);
|
|
1668
2982
|
const offset_location = this._getMapCenterOffset(center, zoom);
|
|
1669
2983
|
view.animate({
|
|
1670
2984
|
center: this._toViewCoords(offset_location),
|
|
1671
2985
|
zoom: zoom,
|
|
1672
|
-
duration:
|
|
1673
|
-
|
|
2986
|
+
duration: prefersReducedMotion()
|
|
2987
|
+
? 0
|
|
2988
|
+
: (duration ?? this._transition_duration),
|
|
2989
|
+
easing: this._easing,
|
|
1674
2990
|
});
|
|
1675
2991
|
}
|
|
1676
2992
|
}
|
|
@@ -1690,14 +3006,7 @@ export default class OpenLayers extends Map {
|
|
|
1690
3006
|
if (!this._mini_map) return;
|
|
1691
3007
|
const overview = this._mini_map.getOverviewMap();
|
|
1692
3008
|
overview.getLayers().clear();
|
|
1693
|
-
|
|
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) {
|
|
3009
|
+
if (!this._tilesAllowed()) {
|
|
1701
3010
|
this._tile_layer_mini = null;
|
|
1702
3011
|
return;
|
|
1703
3012
|
}
|
|
@@ -1707,13 +3016,12 @@ export default class OpenLayers extends Map {
|
|
|
1707
3016
|
const zoomify_pyramid = is_zoomify ? this._zoomifyPyramid() : null;
|
|
1708
3017
|
if (zoomify_pyramid) {
|
|
1709
3018
|
const raw_size = overview.getSize();
|
|
1710
|
-
const size =
|
|
1711
|
-
raw_size && raw_size[0] >= 50 && raw_size[1] >= 50 ? raw_size : [150, 100];
|
|
3019
|
+
const size = raw_size && raw_size[0] >= 50 && raw_size[1] >= 50 ? raw_size : [150, 100];
|
|
1712
3020
|
overview.getView().fit(zoomify_pyramid.extent, { size: size });
|
|
1713
3021
|
} else if (this.bounds_array && this.bounds_array.length) {
|
|
1714
3022
|
this._fitView(overview, this.bounds_array, 0, this._bboxExtent() !== null);
|
|
1715
3023
|
}
|
|
1716
|
-
if (this.
|
|
3024
|
+
if (this.isImageSpace()) {
|
|
1717
3025
|
this._fitMiniMapToImage();
|
|
1718
3026
|
}
|
|
1719
3027
|
}
|
|
@@ -1733,17 +3041,16 @@ export default class OpenLayers extends Map {
|
|
|
1733
3041
|
if (this._tile_layer) {
|
|
1734
3042
|
this._map.removeLayer(this._tile_layer);
|
|
1735
3043
|
}
|
|
1736
|
-
|
|
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
|
-
)) {
|
|
3044
|
+
if (this._tilesAllowed()) {
|
|
1743
3045
|
this._tile_layer = this._createTileLayer(this.options.map_type);
|
|
1744
3046
|
this._tile_layer.setZIndex(0);
|
|
1745
3047
|
this._map.addLayer(this._tile_layer);
|
|
1746
3048
|
}
|
|
3049
|
+
// the story basemap changed: per-slide layers stay cached
|
|
3050
|
+
// (their keys are unchanged) but the active one is rebuilt
|
|
3051
|
+
// by the re-fit below through _switchSlideBasemap
|
|
3052
|
+
this._story_basemap_layer = this._tile_layer ?? null;
|
|
3053
|
+
this._slide_basemap = null;
|
|
1747
3054
|
this._refreshMiniMapLayer();
|
|
1748
3055
|
this._updateAttribution();
|
|
1749
3056
|
this._el.map.style.backgroundColor = this.options.map_background_color;
|
|
@@ -1774,19 +3081,36 @@ export default class OpenLayers extends Map {
|
|
|
1774
3081
|
break;
|
|
1775
3082
|
case "map_bbox": {
|
|
1776
3083
|
// recreate the view so the new extent constraint applies,
|
|
1777
|
-
// preserving center and zoom
|
|
3084
|
+
// preserving center and zoom. The replacement has to
|
|
3085
|
+
// rebuild the *same* view configuration _createMap() uses —
|
|
3086
|
+
// dropping constrainOnlyCenter (which _createMap sets
|
|
3087
|
+
// deliberately, see above), the image zoom ladder or the
|
|
3088
|
+
// legacy zoomify multiWorld flag silently reintroduced the
|
|
3089
|
+
// strict extent constraint this code went out of its way
|
|
3090
|
+
// to avoid, and broke image-mode zoom (issue #465).
|
|
1778
3091
|
const view = this._map.getView();
|
|
1779
3092
|
const center = view.getCenter();
|
|
1780
3093
|
const zoom = view.getZoom();
|
|
1781
3094
|
const extent = this._bboxExtent();
|
|
3095
|
+
// must match _createMap's test exactly: a *georeferenced*
|
|
3096
|
+
// IIIF layer (map_as_image false) is a mercator map, so
|
|
3097
|
+
// testing map_type alone handed it the image-pixel view
|
|
3098
|
+
const is_image_map = this.options.map_type === "iiif" && this.isImageSpace();
|
|
3099
|
+
const user_view = this.options.map_options?.view as
|
|
3100
|
+
Record<string, unknown> | undefined;
|
|
1782
3101
|
this._map.setView(
|
|
1783
3102
|
new View({
|
|
1784
|
-
projection:
|
|
3103
|
+
projection: is_image_map ? "EPSG:4326" : "EPSG:3857",
|
|
1785
3104
|
center: center,
|
|
1786
3105
|
zoom: zoom,
|
|
1787
3106
|
minZoom: view.getMinZoom(),
|
|
1788
3107
|
maxZoom: view.getMaxZoom(),
|
|
1789
|
-
...(
|
|
3108
|
+
...(is_image_map
|
|
3109
|
+
? { multiWorld: true, resolutions: IMAGE_RESOLUTIONS }
|
|
3110
|
+
: {}),
|
|
3111
|
+
...(this.options.map_type === "zoomify" ? { multiWorld: true } : {}),
|
|
3112
|
+
...(extent ? { extent: extent, constrainOnlyCenter: true } : {}),
|
|
3113
|
+
...((user_view ?? {}) as Record<string, unknown>),
|
|
1790
3114
|
}),
|
|
1791
3115
|
);
|
|
1792
3116
|
break;
|
|
@@ -1865,13 +3189,27 @@ export default class OpenLayers extends Map {
|
|
|
1865
3189
|
* stale positions after a size change.
|
|
1866
3190
|
*/
|
|
1867
3191
|
_setViewInstant(loc: StorymapSlideLocation, zoom: number): void {
|
|
1868
|
-
|
|
3192
|
+
const start = this._latLngOf(loc);
|
|
3193
|
+
if (!start) {
|
|
3194
|
+
return;
|
|
3195
|
+
}
|
|
3196
|
+
let _location: LatLngLiteral = start;
|
|
1869
3197
|
if (this.options.map_center_offset) {
|
|
1870
3198
|
_location = this._getMapCenterOffset(_location, zoom);
|
|
1871
3199
|
}
|
|
1872
3200
|
const view = this._map.getView();
|
|
1873
3201
|
view.setZoom(zoom);
|
|
1874
3202
|
view.setCenter(this._toViewCoords(_location));
|
|
3203
|
+
// instant path (resize): rotation sets synchronously, filter and
|
|
3204
|
+
// mask re-apply idempotently — slideshow tours only, so the resize
|
|
3205
|
+
// path for internal documents touches nothing new
|
|
3206
|
+
if (this.options.slideshow_source === true) {
|
|
3207
|
+
const rotation = slideRotationRad(loc);
|
|
3208
|
+
if (rotation !== null || view.getRotation() !== 0) {
|
|
3209
|
+
view.setRotation(rotation ?? 0);
|
|
3210
|
+
}
|
|
3211
|
+
this._applySlideFilterAndMask(loc);
|
|
3212
|
+
}
|
|
1875
3213
|
this._map.renderSync();
|
|
1876
3214
|
}
|
|
1877
3215
|
}
|