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